Merge remote-tracking branch 'origin/main' into multipart-upload

Change-Id: I9b183323cb470185be22f7c648bb76917d2e6fca
This commit is contained in:
Michał Niewrzał 2021-03-10 08:53:38 +01:00
commit 67e26aafcd
104 changed files with 2330 additions and 7175 deletions

View File

@ -1,4 +1,4 @@
GO_VERSION ?= 1.16
GO_VERSION ?= 1.15.7
GOOS ?= linux
GOARCH ?= amd64
GOPATH ?= $(shell go env GOPATH)

View File

@ -211,13 +211,18 @@ func cleanupGEOrphanedData(ctx context.Context, before time.Time) (err error) {
return nil
}
total, err := db.GracefulExit().DeleteAllFinishedTransferQueueItems(ctx, before)
queueTotal, err := db.GracefulExit().DeleteAllFinishedTransferQueueItems(ctx, before)
if err != nil {
fmt.Println("Error, NO ITEMS have been deleted")
fmt.Println("Error, NO ITEMS have been deleted from transfer queue")
return err
}
progressTotal, err := db.GracefulExit().DeleteFinishedExitProgress(ctx, before)
if err != nil {
fmt.Printf("Error, %d stale entries were deleted from exit progress table. More stale entries might remain.\n", progressTotal)
return err
}
fmt.Printf("%d number of items have been deleted from\n", total)
fmt.Printf("%d items have been deleted from transfer queue and %d items deleted from exit progress\n", queueTotal, progressTotal)
return nil
}

View File

@ -0,0 +1,123 @@
# Layer 2 support for SNO payouts
## Context
Recently, the real USD value of per-ERC20 transactions has skyrocketed, both
in terms of GWEI per transaction, but also in terms of the price of ETH.
Like every cryptocurrency, Ethereum's community has been investigating approaches
to scaling transactions, both in reducing cost and increasing throughput.
In the last year, zkRollup-based layer 2 scaling approaches have shown a great
deal of promise. By using SNARKs (or PLONKs), zkRollup-based approaches are able
to support >8000 TPS while still maintaining all of the security guarantees of
the underlying layer 1. [zkSync](https://zksync.io/faq/tech.html) in particular is
a great, usable example. It supports ERC20 tokens, user-driven layer 2 deposits
and withdrawals via an API or a friendly web interface, and can even support
initiating withdrawal to a contract address or other address not explicitly
registered or usable via zkSync.
We're excited about zkRollup (and zkSync in particular) and want to start using
it to dramatically lower transaction costs and improve user experience. With
zkRollup, we may even eventually be able to consider more frequent payouts than
once a month.
Because zkSync is early technology and still has a few rough edges, we don't
want to force uncomfortable users to use it at this time, so we want to give
SNOs the ability to opt in to new layer 2 options.
### Current Payouts system
Our current payouts system has two major components, the satellite and the
CSV-driven payouts pipeline.
The satellite is responsible for generating monthly reports we call
compensation invoices, which are CSVs with the following fields:
```
period,node-id,node-created-at,node-disqualified,node-gracefulexit,node-wallet,
node-address,node-last-ip,codes,usage-at-rest,usage-get,usage-put,
usage-get-repair,usage-put-repair,usage-get-audit,comp-at-rest,comp-get,comp-put,
comp-get-repair,comp-put-repair,comp-get-audit,surge-percent,owed,held,disposed,
total-held,total-disposed,paid-ytd
```
We then pass this CSV to an internal set of tools called the `accountant` which
are responsible for checking these nodes' IP and wallet addresses against export
restrictions and a few other things and ultimately transforming the above data
into a small, two column spreadsheet with just addresses and USD amounts we
should transfer.
```
addr,amnt
```
Once these payouts have been processed, we generate a CSV of receipts (links
to settled transactions hashes) and reimport this data back into the satellite
for that period.
For us to use a different solution than layer 1 transfers, we need to extend
the above pipeline to:
* indicate which transactions should happen on layer 2
* indicate what type of receipt a receipt is
## Design goals
* Whenever a SNO configures a wallet address, we want that SNO to be able to
additionally flag what features that wallet address supports (initially,
opt-in zkSync support, but potentially more in the future).
* The satellite should keep track of per-node wallet addresses along with what
features the wallet supports.
* Two storage nodes that share the same wallet address will not necessarily
have the same feature flags (we want to support a SNO choosing to experiment
with zkSync on one node but not on another).
* Transaction references to completed payouts should indicate which technology
was used with that transaction, so zkSync transactions can be displayed in the
SNO dashboard differently than layer 1 transactions.
## Implementation notes
* Matter Labs has already provided us with a tool that processes CSVs of the
form `addr,amnt` and will generate receipts in our format, so the actual
integration with zkSync is already done for our pipeline. We only need to
know when to use their tool vs ours.
* Docker storage nodes currently configure wallet addresses with an environment
variable. We should configure supported wallet technologies alongside this
environment variable. For example:
WALLET="0x..." WALLET_FEATURES="zksync,raiden"
The storage node should confirm that the list of wallet features is a
comma-separated list of strings.
* Windows configuration is obviously different. Each platform needs a way to
have a user add support for some wallet features. We can start off with
config file only and add UI features soon thereafter.
* This list of wallet features should be sent through the contact.NodeInfo
struct to the Satellite, and should be stored on the Satellite in the nodes
table.
* We want this column to be outputted per node during the generate-invoices
subcommand of the compensation subcommand of the satellite, so
"wallet-features" will need to be added to the invoices CSV.
* Our accountant pipeline currently aggregates all payments to a single
wallet address. We'll need to change our accountant pipeline to output
a different CSV per transaction method (zkSync vs normal layer 1). This
means that if a user has the same wallet on two nodes, but only one node
opts-in to zkSync, then that wallet will get two payouts, one with zkSync
and one without. We will only aggregate within a specific method.
In a scenario where an operator has three nodes, one with no wallet features,
one with the `zksync` wallet feature, and one with the `zksync,raiden`
wallet features, it will be up to the `accountant` tool to choose whether
or not the third node gets a payout via Raiden or zkSync based on what
we prefer. If the `accountant` tool prefers `zksync` over `raiden`, then
the operator will get two transactions: one layer 1, and one combined `zksync`
payout. If the `accountant` tool prefers `raiden` over `zksync`, then that
operator would get three transactions.
## Future compatibility and plans
* We want zkSync to be opt-in for now, but we expect at some future point to
be opt-out when zkSync and our community are ready.
* Even though zkSync is opt-in for now, we want it to be prominent, in that
we want to encourage people to use it if they are willing.
* If at some point we decide we want to add a new wallet feature (e.g.
"plasma"), we should not require storage node or satellite code changes to
get that wallet feature indication out of the compensation CSV.

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
github.com/shopspring/decimal v1.2.0
github.com/spacemonkeygo/monkit/v3 v3.0.7
github.com/spacemonkeygo/monkit/v3 v3.0.9
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1

3
go.sum
View File

@ -531,8 +531,9 @@ github.com/spacemonkeygo/monkit/v3 v3.0.0-20191108235033-eacca33b3037/go.mod h1:
github.com/spacemonkeygo/monkit/v3 v3.0.4/go.mod h1:JcK1pCbReQsOsMKF/POFSZCq7drXFybgGmbc27tuwes=
github.com/spacemonkeygo/monkit/v3 v3.0.5/go.mod h1:JcK1pCbReQsOsMKF/POFSZCq7drXFybgGmbc27tuwes=
github.com/spacemonkeygo/monkit/v3 v3.0.7-0.20200515175308-072401d8c752/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=
github.com/spacemonkeygo/monkit/v3 v3.0.7 h1:LsGdIXl8mccqJrYEh4Uf4sLVGu/g0tjhNqQzdn9MzVk=
github.com/spacemonkeygo/monkit/v3 v3.0.7/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=
github.com/spacemonkeygo/monkit/v3 v3.0.9 h1:tGdClugDxCa4OaB8cvOLcgLCCC1VwBls17rvJWJ3V88=
github.com/spacemonkeygo/monkit/v3 v3.0.9/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=
github.com/spacemonkeygo/monotime v0.0.0-20180824235756-e3f48a95f98a/go.mod h1:ul4bvvnCOPZgq8w0nTkSmWVg/hauVpFS97Am1YM1XXo=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=

View File

@ -7,6 +7,7 @@ import (
"net"
"time"
quicgo "github.com/lucas-clemente/quic-go"
"github.com/zeebo/errs"
"storj.io/common/netutil"
@ -17,6 +18,18 @@ import (
// defaultUserTimeout is the value we use for the TCP_USER_TIMEOUT setting.
const defaultUserTimeout = 60 * time.Second
// defaultQUICConfig is the value we use for QUIC setting.
func defaultQUICConfig() *quicgo.Config {
return &quicgo.Config{
MaxIdleTimeout: defaultUserTimeout,
// disable address validation in QUIC (it costs an extra round-trip, and we believe
// it to be unnecessary given the low potential for traffic amplification attacks).
AcceptToken: func(clientAddr net.Addr, token *quicgo.Token) bool {
return true
},
}
}
// wrapListener wraps the provided net.Listener in one that sets timeouts
// and monitors if the returned connections are closed or leaked.
func wrapListener(lis net.Listener) net.Listener {

View File

@ -13,7 +13,6 @@ import (
"sync"
"syscall"
quicgo "github.com/lucas-clemente/quic-go"
"github.com/zeebo/errs"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
@ -171,7 +170,7 @@ func (p *Server) Run(ctx context.Context) (err error) {
}
if p.public.udpConn != nil {
p.public.quicListener, err = quic.NewListener(p.public.udpConn, p.tlsOptions.ServerTLSConfig(), &quicgo.Config{MaxIdleTimeout: defaultUserTimeout})
p.public.quicListener, err = quic.NewListener(p.public.udpConn, p.tlsOptions.ServerTLSConfig(), defaultQUICConfig())
if err != nil {
return err
}

View File

@ -6,14 +6,11 @@ package pgutil
import (
"context"
"strings"
"time"
"github.com/jackc/pgtype"
"github.com/jackc/pgx/v4"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"storj.io/common/storj"
"storj.io/storj/private/dbutil"
"storj.io/storj/private/dbutil/dbschema"
"storj.io/storj/private/dbutil/pgutil/pgerrcode"
@ -115,130 +112,6 @@ func IsConstraintError(err error) bool {
return strings.HasPrefix(errCode, pgErrorClassConstraintViolation)
}
// The following XArray() helper methods exist alongside similar methods in the
// jackc/pgtype library. The difference with the methods in pgtype is that they
// will accept any of a wide range of types. That is nice, but it comes with
// the potential that someone might pass in an invalid type; thus, those
// methods have to return (*pgtype.XArray, error).
//
// The methods here do not need to return an error because they require passing
// in the correct type to begin with.
//
// An alternative implementation for the following methods might look like
// calls to pgtype.ByteaArray() followed by `if err != nil { panic }` blocks.
// That would probably be ok, but we decided on this approach, as it ought to
// require fewer allocations and less time, in addition to having no error
// return.
// ByteaArray returns an object usable by pg drivers for passing a [][]byte slice
// into a database as type BYTEA[].
func ByteaArray(bytesArray [][]byte) *pgtype.ByteaArray {
pgtypeByteaArray := make([]pgtype.Bytea, len(bytesArray))
for i, byteSlice := range bytesArray {
pgtypeByteaArray[i].Bytes = byteSlice
pgtypeByteaArray[i].Status = pgtype.Present
}
return &pgtype.ByteaArray{
Elements: pgtypeByteaArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(bytesArray)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// TextArray returns an object usable by pg drivers for passing a []string slice
// into a database as type TEXT[].
func TextArray(stringSlice []string) *pgtype.TextArray {
pgtypeTextArray := make([]pgtype.Text, len(stringSlice))
for i, s := range stringSlice {
pgtypeTextArray[i].String = s
pgtypeTextArray[i].Status = pgtype.Present
}
return &pgtype.TextArray{
Elements: pgtypeTextArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(stringSlice)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// TimestampTZArray returns an object usable by pg drivers for passing a []time.Time
// slice into a database as type TIMESTAMPTZ[].
func TimestampTZArray(timeSlice []time.Time) *pgtype.TimestamptzArray {
pgtypeTimestamptzArray := make([]pgtype.Timestamptz, len(timeSlice))
for i, t := range timeSlice {
pgtypeTimestamptzArray[i].Time = t
pgtypeTimestamptzArray[i].Status = pgtype.Present
}
return &pgtype.TimestamptzArray{
Elements: pgtypeTimestamptzArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(timeSlice)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Int4Array returns an object usable by pg drivers for passing a []int32 slice
// into a database as type INT4[].
func Int4Array(ints []int32) *pgtype.Int4Array {
pgtypeInt4Array := make([]pgtype.Int4, len(ints))
for i, someInt := range ints {
pgtypeInt4Array[i].Int = someInt
pgtypeInt4Array[i].Status = pgtype.Present
}
return &pgtype.Int4Array{
Elements: pgtypeInt4Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(ints)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Int8Array returns an object usable by pg drivers for passing a []int64 slice
// into a database as type INT8[].
func Int8Array(bigInts []int64) *pgtype.Int8Array {
pgtypeInt8Array := make([]pgtype.Int8, len(bigInts))
for i, bigInt := range bigInts {
pgtypeInt8Array[i].Int = bigInt
pgtypeInt8Array[i].Status = pgtype.Present
}
return &pgtype.Int8Array{
Elements: pgtypeInt8Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(bigInts)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Float8Array returns an object usable by pg drivers for passing a []float64 slice
// into a database as type FLOAT8[].
func Float8Array(floats []float64) *pgtype.Float8Array {
pgtypeFloat8Array := make([]pgtype.Float8, len(floats))
for i, someFloat := range floats {
pgtypeFloat8Array[i].Float = someFloat
pgtypeFloat8Array[i].Status = pgtype.Present
}
return &pgtype.Float8Array{
Elements: pgtypeFloat8Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(floats)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// NodeIDArray returns an object usable by pg drivers for passing a []storj.NodeID
// slice into a database as type BYTEA[].
func NodeIDArray(nodeIDs []storj.NodeID) *pgtype.ByteaArray {
if nodeIDs == nil {
return &pgtype.ByteaArray{Status: pgtype.Null}
}
pgtypeByteaArray := make([]pgtype.Bytea, len(nodeIDs))
for i, nodeID := range nodeIDs {
nodeIDCopy := nodeID
pgtypeByteaArray[i].Bytes = nodeIDCopy[:]
pgtypeByteaArray[i].Status = pgtype.Present
}
return &pgtype.ByteaArray{
Elements: pgtypeByteaArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(nodeIDs)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// QuoteIdentifier quotes an identifier for use in an interpolated SQL string.
func QuoteIdentifier(ident string) string {
return pgx.Identifier{ident}.Sanitize()

View File

@ -0,0 +1,156 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package pgutil
import (
"time"
"github.com/jackc/pgtype"
"storj.io/common/storj"
"storj.io/common/uuid"
)
// The following XArray() helper methods exist alongside similar methods in the
// jackc/pgtype library. The difference with the methods in pgtype is that they
// will accept any of a wide range of types. That is nice, but it comes with
// the potential that someone might pass in an invalid type; thus, those
// methods have to return (*pgtype.XArray, error).
//
// The methods here do not need to return an error because they require passing
// in the correct type to begin with.
//
// An alternative implementation for the following methods might look like
// calls to pgtype.ByteaArray() followed by `if err != nil { panic }` blocks.
// That would probably be ok, but we decided on this approach, as it ought to
// require fewer allocations and less time, in addition to having no error
// return.
// ByteaArray returns an object usable by pg drivers for passing a [][]byte slice
// into a database as type BYTEA[].
func ByteaArray(bytesArray [][]byte) *pgtype.ByteaArray {
pgtypeByteaArray := make([]pgtype.Bytea, len(bytesArray))
for i, byteSlice := range bytesArray {
pgtypeByteaArray[i].Bytes = byteSlice
pgtypeByteaArray[i].Status = pgtype.Present
}
return &pgtype.ByteaArray{
Elements: pgtypeByteaArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(bytesArray)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// TextArray returns an object usable by pg drivers for passing a []string slice
// into a database as type TEXT[].
func TextArray(stringSlice []string) *pgtype.TextArray {
pgtypeTextArray := make([]pgtype.Text, len(stringSlice))
for i, s := range stringSlice {
pgtypeTextArray[i].String = s
pgtypeTextArray[i].Status = pgtype.Present
}
return &pgtype.TextArray{
Elements: pgtypeTextArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(stringSlice)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// TimestampTZArray returns an object usable by pg drivers for passing a []time.Time
// slice into a database as type TIMESTAMPTZ[].
func TimestampTZArray(timeSlice []time.Time) *pgtype.TimestamptzArray {
pgtypeTimestamptzArray := make([]pgtype.Timestamptz, len(timeSlice))
for i, t := range timeSlice {
pgtypeTimestamptzArray[i].Time = t
pgtypeTimestamptzArray[i].Status = pgtype.Present
}
return &pgtype.TimestamptzArray{
Elements: pgtypeTimestamptzArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(timeSlice)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Int4Array returns an object usable by pg drivers for passing a []int32 slice
// into a database as type INT4[].
func Int4Array(ints []int32) *pgtype.Int4Array {
pgtypeInt4Array := make([]pgtype.Int4, len(ints))
for i, someInt := range ints {
pgtypeInt4Array[i].Int = someInt
pgtypeInt4Array[i].Status = pgtype.Present
}
return &pgtype.Int4Array{
Elements: pgtypeInt4Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(ints)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Int8Array returns an object usable by pg drivers for passing a []int64 slice
// into a database as type INT8[].
func Int8Array(bigInts []int64) *pgtype.Int8Array {
pgtypeInt8Array := make([]pgtype.Int8, len(bigInts))
for i, bigInt := range bigInts {
pgtypeInt8Array[i].Int = bigInt
pgtypeInt8Array[i].Status = pgtype.Present
}
return &pgtype.Int8Array{
Elements: pgtypeInt8Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(bigInts)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// Float8Array returns an object usable by pg drivers for passing a []float64 slice
// into a database as type FLOAT8[].
func Float8Array(floats []float64) *pgtype.Float8Array {
pgtypeFloat8Array := make([]pgtype.Float8, len(floats))
for i, someFloat := range floats {
pgtypeFloat8Array[i].Float = someFloat
pgtypeFloat8Array[i].Status = pgtype.Present
}
return &pgtype.Float8Array{
Elements: pgtypeFloat8Array,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(floats)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// NodeIDArray returns an object usable by pg drivers for passing a []storj.NodeID
// slice into a database as type BYTEA[].
func NodeIDArray(nodeIDs []storj.NodeID) *pgtype.ByteaArray {
if nodeIDs == nil {
return &pgtype.ByteaArray{Status: pgtype.Null}
}
pgtypeByteaArray := make([]pgtype.Bytea, len(nodeIDs))
for i, nodeID := range nodeIDs {
nodeIDCopy := nodeID
pgtypeByteaArray[i].Bytes = nodeIDCopy[:]
pgtypeByteaArray[i].Status = pgtype.Present
}
return &pgtype.ByteaArray{
Elements: pgtypeByteaArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(nodeIDs)), LowerBound: 1}},
Status: pgtype.Present,
}
}
// UUIDArray returns an object usable by pg drivers for passing a []uuid.UUID
// slice into a database as type BYTEA[].
func UUIDArray(uuids []uuid.UUID) *pgtype.ByteaArray {
if uuids == nil {
return &pgtype.ByteaArray{Status: pgtype.Null}
}
pgtypeByteaArray := make([]pgtype.Bytea, len(uuids))
for i, uuid := range uuids {
uuidCopy := uuid
pgtypeByteaArray[i].Bytes = uuidCopy[:]
pgtypeByteaArray[i].Status = pgtype.Present
}
return &pgtype.ByteaArray{
Elements: pgtypeByteaArray,
Dimensions: []pgtype.ArrayDimension{{Length: int32(len(uuids)), LowerBound: 1}},
Status: pgtype.Present,
}
}

View File

@ -28,6 +28,7 @@ import (
"storj.io/private/version"
"storj.io/storj/pkg/revocation"
"storj.io/storj/pkg/server"
"storj.io/storj/private/testredis"
versionchecker "storj.io/storj/private/version/checker"
"storj.io/storj/private/web"
"storj.io/storj/satellite"
@ -61,7 +62,6 @@ import (
"storj.io/storj/satellite/repair/irreparable"
"storj.io/storj/satellite/repair/repairer"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
"storj.io/storj/storage/redis/redisserver"
)
// Satellite contains all the processes needed to run a full Satellite setup.
@ -386,7 +386,7 @@ func (planet *Planet) newSatellite(ctx context.Context, prefix string, index int
}
planet.databases = append(planet.databases, metabaseDB)
redis, err := redisserver.Mini(ctx)
redis, err := testredis.Mini(ctx)
if err != nil {
return nil, err
}

View File

@ -1,8 +1,8 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
// Package redisserver is package for starting a redis test server
package redisserver
// Package testredis is package for starting a redis test server
package testredis
import (
"bufio"
@ -36,13 +36,13 @@ const (
type Server interface {
Addr() string
Close() error
// TestingFastForward is a function for enforce the TTL of keys in
// FastForward is a function for enforce the TTL of keys in
// implementations what they have not exercise the expiration by themselves
// (e.g. Minitredis). This method is a no-op in implementations which support
// the expiration as usual.
//
// All the keys whose TTL minus d become <= 0 will be removed.
TestingFastForward(d time.Duration)
FastForward(d time.Duration)
}
func freeport() (addr string, port int) {
@ -167,7 +167,7 @@ func (process *process) Close() error {
return nil
}
func (process *process) TestingFastForward(_ time.Duration) {}
func (process *process) FastForward(_ time.Duration) {}
func pingServer(addr string) error {
client := redis.NewClient(&redis.Options{Addr: addr, DB: 1})
@ -200,6 +200,6 @@ func (s *miniserver) Close() error {
return nil
}
func (s *miniserver) TestingFastForward(d time.Duration) {
s.FastForward(d)
func (s *miniserver) FastForward(d time.Duration) {
s.Miniredis.FastForward(d)
}

View File

@ -11,8 +11,8 @@ import (
"storj.io/common/peertls/extensions"
"storj.io/common/testcontext"
"storj.io/storj/pkg/revocation"
"storj.io/storj/private/testredis"
"storj.io/storj/storage"
"storj.io/storj/storage/redis/redisserver"
)
// RunDBs runs the passed test function with each type of revocation database.
@ -21,7 +21,7 @@ func RunDBs(t *testing.T, test func(*testing.T, extensions.RevocationDB, storage
ctx := testcontext.New(t)
defer ctx.Cleanup()
redis, err := redisserver.Mini(ctx)
redis, err := testredis.Mini(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)

View File

@ -17,9 +17,9 @@ import (
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/testredis"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/accounting/live"
"storj.io/storj/storage/redis/redisserver"
)
func TestAddGetProjectStorageAndBandwidthUsage(t *testing.T) {
@ -33,7 +33,7 @@ func TestAddGetProjectStorageAndBandwidthUsage(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
redis, err := redisserver.Start(ctx)
redis, err := testredis.Start(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)
@ -107,7 +107,7 @@ func TestGetAllProjectTotals(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
redis, err := redisserver.Start(ctx)
redis, err := testredis.Start(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)
@ -159,7 +159,7 @@ func TestLiveAccountingCache_ProjectBandwidthUsage_expiration(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
redis, err := redisserver.Start(ctx)
redis, err := testredis.Start(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)
@ -188,7 +188,7 @@ func TestLiveAccountingCache_ProjectBandwidthUsage_expiration(t *testing.T) {
require.NoError(t, err)
if tt.backend == "redis" {
redis.TestingFastForward(time.Second)
redis.FastForward(time.Second)
}
time.Sleep(2 * time.Second)

View File

@ -349,6 +349,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
failed
contained
unknown
remove
erred
)
@ -400,7 +401,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
})
if err != nil {
if storj.ErrObjectNotFound.Has(err) {
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
return
}
@ -411,7 +412,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
if pendingObject.ExpiresAt != nil && !pendingObject.ExpiresAt.IsZero() && pendingObject.ExpiresAt.Before(verifier.nowFn()) {
verifier.log.Debug("Reverify: segment already expired", zap.Stringer("Node ID", pending.NodeID))
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
return
}
@ -432,7 +433,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
// TODO: is this check still necessary? If the segment was found by its StreamID and position, the RootPieceID should not had changed.
if pendingSegmentInfo.RootPieceID != pending.PieceID {
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
return
}
var pieceNum uint16
@ -444,7 +445,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
}
}
if !found {
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
return
}
@ -517,7 +518,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
// Get the original segment
err := verifier.checkIfSegmentAltered(ctx, pending.Segment, pendingSegmentInfo)
if err != nil {
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
verifier.log.Debug("Reverify: audit source changed before reverification", zap.Stringer("Node ID", pending.NodeID), zap.Error(err))
return
}
@ -544,7 +545,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
} else {
err := verifier.checkIfSegmentAltered(ctx, pending.Segment, pendingSegmentInfo)
if err != nil {
ch <- result{nodeID: pending.NodeID, status: skipped}
ch <- result{nodeID: pending.NodeID, status: remove}
verifier.log.Debug("Reverify: audit source changed before reverification", zap.Stringer("Node ID", pending.NodeID), zap.Error(err))
return
}
@ -558,6 +559,7 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
for range pieces {
result := <-ch
switch result.status {
case skipped:
case success:
report.Successes = append(report.Successes, result.nodeID)
case offline:
@ -568,13 +570,14 @@ func (verifier *Verifier) Reverify(ctx context.Context, segment Segment) (report
report.PendingAudits = append(report.PendingAudits, result.pendingAudit)
case unknown:
report.Unknown = append(report.Unknown, result.nodeID)
case skipped:
case remove:
_, errDelete := verifier.containment.Delete(ctx, result.nodeID)
if errDelete != nil {
verifier.log.Debug("Error deleting node from containment db", zap.Stringer("Node ID", result.nodeID), zap.Error(errDelete))
}
case erred:
err = errs.Combine(err, result.err)
default:
}
}

View File

@ -18,6 +18,7 @@ import (
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/post"
"storj.io/storj/private/testredis"
"storj.io/storj/satellite"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/accounting/live"
@ -29,7 +30,6 @@ import (
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
"storj.io/storj/storage/redis/redisserver"
)
// discardSender discard sending of an actual email.
@ -59,7 +59,7 @@ func TestGraphqlMutation(t *testing.T) {
},
)
redis, err := redisserver.Mini(ctx)
redis, err := testredis.Mini(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)

View File

@ -15,6 +15,7 @@ import (
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/testredis"
"storj.io/storj/satellite"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/accounting/live"
@ -26,7 +27,6 @@ import (
"storj.io/storj/satellite/payments/stripecoinpayments"
"storj.io/storj/satellite/rewards"
"storj.io/storj/satellite/satellitedb/satellitedbtest"
"storj.io/storj/storage/redis/redisserver"
)
func TestGraphqlQuery(t *testing.T) {
@ -43,7 +43,7 @@ func TestGraphqlQuery(t *testing.T) {
},
)
redis, err := redisserver.Mini(ctx)
redis, err := testredis.Mini(ctx)
require.NoError(t, err)
defer ctx.Check(redis.Close)
@ -219,7 +219,6 @@ func TestGraphqlQuery(t *testing.T) {
err = service.ActivateAccount(ctx, activationToken1)
require.NoError(t, err)
user1.Email = "muu1@mail.test"
})
regTokenUser2, err := service.CreateRegToken(ctx, 2)

View File

@ -82,6 +82,7 @@ type Config struct {
GeneralRequestURL string `help:"url link to general request page" default:"https://support.tardigrade.io/hc/en-us/requests/new?ticket_form_id=360000379291"`
ProjectLimitsIncreaseRequestURL string `help:"url link to project limit increase request page" default:"https://support.tardigrade.io/hc/en-us/requests/new?ticket_form_id=360000683212"`
GatewayCredentialsRequestURL string `help:"url link for gateway credentials requests" default:"https://auth.tardigradeshare.io"`
IsBetaSatellite bool `help:"indicates if satellite is in beta" default:"false"`
RateLimit web.IPRateLimiterConfig
@ -288,6 +289,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
GeneralRequestURL string
ProjectLimitsIncreaseRequestURL string
GatewayCredentialsRequestURL string
IsBetaSatellite bool
}
data.ExternalAddress = server.config.ExternalAddress
@ -302,6 +304,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
data.GeneralRequestURL = server.config.GeneralRequestURL
data.ProjectLimitsIncreaseRequestURL = server.config.ProjectLimitsIncreaseRequestURL
data.GatewayCredentialsRequestURL = server.config.GatewayCredentialsRequestURL
data.IsBetaSatellite = server.config.IsBetaSatellite
if server.templates.index == nil {
server.log.Error("index template is not set")

View File

@ -553,12 +553,16 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
}
newUser := &User{
ID: userID,
Email: user.Email,
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
Status: Inactive,
ID: userID,
Email: user.Email,
FullName: user.FullName,
ShortName: user.ShortName,
PasswordHash: hash,
Status: Inactive,
IsProfessional: user.IsProfessional,
Position: user.Position,
CompanyName: user.CompanyName,
EmployeeCount: user.EmployeeCount,
}
if user.PartnerID != "" {
newUser.PartnerID, err = uuid.FromString(user.PartnerID)

View File

@ -59,6 +59,11 @@ type DB interface {
// queue items whose nodes have finished the exit before the indicated time
// returning the total number of deleted items.
DeleteAllFinishedTransferQueueItems(ctx context.Context, before time.Time) (count int64, err error)
// DeleteFinishedExitProgress deletes exit progress entries for nodes that
// finished exiting before the indicated time, returns number of deleted entries.
DeleteFinishedExitProgress(ctx context.Context, before time.Time) (count int64, err error)
// GetFinishedExitNodes gets nodes that are marked having finished graceful exit before a given time.
GetFinishedExitNodes(ctx context.Context, before time.Time) (finishedNodes []storj.NodeID, err error)
// GetTransferQueueItem gets a graceful exit transfer queue entry.
GetTransferQueueItem(ctx context.Context, nodeID storj.NodeID, key metabase.SegmentKey, pieceNum int32) (*TransferQueueItem, error)
// GetIncomplete gets incomplete graceful exit transfer queue entries ordered by durability ratio and queued date ascending.

View File

@ -19,6 +19,58 @@ import (
"storj.io/storj/satellite/overlay"
)
func TestGracefulexitDB_DeleteFinishedExitProgress(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 6,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
satellite := planet.Satellites[0]
geDB := planet.Satellites[0].DB.GracefulExit()
days := 6
currentTime := time.Now().UTC()
// Set timestamp back by the number of days we want to save
timestamp := currentTime.AddDate(0, 0, -days).Truncate(time.Millisecond)
for i := 0; i < days; i++ {
nodeID := planet.StorageNodes[i].ID()
err := geDB.IncrementProgress(ctx, nodeID, 100, 100, 100)
require.NoError(t, err)
_, err = satellite.Overlay.DB.UpdateExitStatus(ctx, &overlay.ExitStatusRequest{
NodeID: nodeID,
ExitFinishedAt: timestamp,
})
require.NoError(t, err)
// Advance time by 24 hours
timestamp = timestamp.Add(time.Hour * 24)
}
threeDays := currentTime.AddDate(0, 0, -days/2).Add(-time.Millisecond)
finishedNodes, err := geDB.GetFinishedExitNodes(ctx, threeDays)
require.NoError(t, err)
require.Len(t, finishedNodes, 3)
finishedNodes, err = geDB.GetFinishedExitNodes(ctx, currentTime)
require.NoError(t, err)
require.Len(t, finishedNodes, 6)
count, err := geDB.DeleteFinishedExitProgress(ctx, threeDays)
require.NoError(t, err)
require.EqualValues(t, 3, count)
// Check that first three nodes were removed from exit progress table
for i, node := range planet.StorageNodes {
progress, err := geDB.GetProgress(ctx, node.ID())
if i < 3 {
require.True(t, gracefulexit.ErrNodeNotFound.Has(err))
require.Nil(t, progress)
} else {
require.NoError(t, err)
}
}
})
}
func TestGracefulExit_DeleteAllFinishedTransferQueueItems(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 7,

View File

@ -34,7 +34,7 @@ var (
type Config struct {
EncryptionKeys EncryptionKeys `help:"encryption keys to encrypt info in orders" default:""`
Expiration time.Duration `help:"how long until an order expires" default:"48h"` // 2 days
FlushBatchSize int `help:"how many items in the rollups write cache before they are flushed to the database" devDefault:"20" releaseDefault:"10000"`
FlushBatchSize int `help:"how many items in the rollups write cache before they are flushed to the database" devDefault:"20" releaseDefault:"1000"`
FlushInterval time.Duration `help:"how often to flush the rollups write cache to the database" devDefault:"30s" releaseDefault:"1m"`
NodeStatusLogging bool `hidden:"true" help:"deprecated, log the offline/disqualification status of nodes" default:"false"`
OrdersSemaphoreSize int `help:"how many concurrent orders to process at once. zero is unlimited" default:"2"`

View File

@ -155,7 +155,7 @@ func (m *mockCustomers) repopulate() error {
}
for cusPage.Next {
cusPage, err := m.customersDB.List(ctx, cusPage.NextOffset, limit, time.Now())
cusPage, err = m.customersDB.List(ctx, cusPage.NextOffset, limit, time.Now())
if err != nil {
return err
}

View File

@ -47,27 +47,66 @@ func (comp *compensationDB) QueryTotalAmounts(ctx context.Context, nodeID storj.
func (comp *compensationDB) RecordPeriod(ctx context.Context, paystubs []compensation.Paystub, payments []compensation.Payment) (err error) {
defer mon.Task()(&ctx)(&err)
return Error.Wrap(comp.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
if err := recordPaystubs(ctx, tx, paystubs); err != nil {
return err
}
if err := recordPayments(ctx, tx, payments); err != nil {
return err
}
return nil
}))
if err := comp.RecordPaystubs(ctx, paystubs); err != nil {
return err
}
if err := comp.RecordPayments(ctx, payments); err != nil {
return err
}
return nil
}
func stringPointersEqual(a, b *string) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
func (comp *compensationDB) RecordPayments(ctx context.Context, payments []compensation.Payment) (err error) {
defer mon.Task()(&ctx)(&err)
return Error.Wrap(comp.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
return recordPayments(ctx, tx, payments)
}))
for _, payment := range payments {
payment := payment // to satisfy linting
err := comp.db.WithTx(ctx, func(ctx context.Context, tx *dbx.Tx) error {
existingPayments, err := tx.All_StoragenodePayment_By_NodeId_And_Period(ctx,
dbx.StoragenodePayment_NodeId(payment.NodeID.Bytes()),
dbx.StoragenodePayment_Period(payment.Period.String()))
if err != nil {
return Error.Wrap(err)
}
// check if the payment already exists. we know period and node id already match.
for _, existingPayment := range existingPayments {
if existingPayment.Amount == payment.Amount.Value() &&
stringPointersEqual(existingPayment.Receipt, payment.Receipt) &&
stringPointersEqual(existingPayment.Notes, payment.Notes) {
return nil
}
}
return Error.Wrap(tx.CreateNoReturn_StoragenodePayment(ctx,
dbx.StoragenodePayment_NodeId(payment.NodeID.Bytes()),
dbx.StoragenodePayment_Period(payment.Period.String()),
dbx.StoragenodePayment_Amount(payment.Amount.Value()),
dbx.StoragenodePayment_Create_Fields{
Receipt: dbx.StoragenodePayment_Receipt_Raw(payment.Receipt),
Notes: dbx.StoragenodePayment_Notes_Raw(payment.Notes),
},
))
})
if err != nil {
return err
}
}
return nil
}
func recordPaystubs(ctx context.Context, tx *dbx.Tx, paystubs []compensation.Paystub) error {
func (comp *compensationDB) RecordPaystubs(ctx context.Context, paystubs []compensation.Paystub) error {
for _, paystub := range paystubs {
err := tx.CreateNoReturn_StoragenodePaystub(ctx,
err := comp.db.ReplaceNoReturn_StoragenodePaystub(ctx,
dbx.StoragenodePaystub_Period(paystub.Period.String()),
dbx.StoragenodePaystub_NodeId(paystub.NodeID.Bytes()),
dbx.StoragenodePaystub_Codes(paystub.Codes.String()),
@ -96,25 +135,3 @@ func recordPaystubs(ctx context.Context, tx *dbx.Tx, paystubs []compensation.Pay
}
return nil
}
func recordPayments(ctx context.Context, tx *dbx.Tx, payments []compensation.Payment) error {
for _, payment := range payments {
opts := dbx.StoragenodePayment_Create_Fields{}
if payment.Receipt != nil {
opts.Receipt = dbx.StoragenodePayment_Receipt(*payment.Receipt)
}
if payment.Notes != nil {
opts.Notes = dbx.StoragenodePayment_Notes(*payment.Notes)
}
err := tx.CreateNoReturn_StoragenodePayment(ctx,
dbx.StoragenodePayment_NodeId(payment.NodeID.Bytes()),
dbx.StoragenodePayment_Period(payment.Period.String()),
dbx.StoragenodePayment_Amount(payment.Amount.Value()),
opts,
)
if err != nil {
return err
}
}
return nil
}

View File

@ -11,10 +11,6 @@ model value_attribution (
)
create value_attribution ()
delete value_attribution (
where value_attribution.project_id = ?
where value_attribution.bucket_name = ?
)
read one (
select value_attribution
@ -35,8 +31,6 @@ model pending_audits (
field path blob
)
create pending_audits ( )
update pending_audits ( where pending_audits.node_id = ? )
delete pending_audits ( where pending_audits.node_id = ? )
read one (
select pending_audits
@ -110,11 +104,6 @@ model accounting_rollup (
create accounting_rollup ( noreturn, replace )
read all (
select accounting_rollup
where accounting_rollup.start_time >= ?
)
//--- overlay cache ---//
model node (
@ -157,7 +146,7 @@ model node (
field vetted_at timestamp ( updatable, nullable )
field uptime_success_count int64 ( updatable, default 0 )
field total_uptime_count int64 ( updatable, default 0 )
field created_at timestamp ( autoinsert, default current_timestamp )
field updated_at timestamp ( autoinsert, autoupdate, default current_timestamp )
field last_contact_success timestamp ( updatable, default "epoch" )
@ -182,9 +171,6 @@ model node (
// unknown_audit_reputation fields track information related to successful vs. unknown error audits
field unknown_audit_reputation_alpha float64 ( updatable, default 1 )
field unknown_audit_reputation_beta float64 ( updatable, default 0 )
// TODO remove uptime alpha/beta once old uptime dq code is removed
field uptime_reputation_alpha float64 ( updatable, default 1 )
field uptime_reputation_beta float64 ( updatable, default 0 )
field exit_initiated_at timestamp ( updatable, nullable )
field exit_loop_completed_at timestamp ( updatable, nullable )
@ -192,13 +178,11 @@ model node (
field exit_success bool ( updatable, default false )
)
create node ( noreturn )
update node ( where node.id = ? )
update node (
where node.id = ?
noreturn
)
delete node ( where node.id = ? )
// "Get" query; fails if node not found
read one (
@ -210,32 +194,11 @@ read all (
select node.id
)
read limitoffset (
select node
where node.id >= ?
orderby asc node.id
)
read all (
select node.id node.piece_count
where node.piece_count != 0
)
read limitoffset (
select node.id node.address node.last_ip_port node.last_contact_success node.last_contact_failure
where node.last_contact_success < node.last_contact_failure
where node.disqualified = null
orderby asc node.last_contact_failure
)
read all (
select node.id node.address node.last_ip_port node.last_contact_success node.last_contact_failure
where node.last_contact_success < ?
where node.last_contact_success > node.last_contact_failure
where node.disqualified = null
orderby asc node.last_contact_success
)
//--- audit history ---//
model audit_history (
key node_id
@ -406,10 +369,6 @@ read all (
select project_member
where project_member.member_id = ?
)
read limitoffset (
select project_member
where project_member.project_id = ?
)
model api_key (
key id
@ -445,11 +404,6 @@ read one (
where api_key.name = ?
where api_key.project_id = ?
)
read all (
select api_key
where api_key.project_id = ?
orderby asc api_key.name
)
// --- bucket accounting tables --- //
@ -476,14 +430,6 @@ model bucket_bandwidth_rollup (
field settled uint64 ( updatable )
)
read scalar (
select bucket_bandwidth_rollup
where bucket_bandwidth_rollup.bucket_name = ?
where bucket_bandwidth_rollup.project_id = ?
where bucket_bandwidth_rollup.interval_start = ?
where bucket_bandwidth_rollup.action = ?
)
read paged (
select bucket_bandwidth_rollup
where bucket_bandwidth_rollup.interval_start >= ?
@ -533,12 +479,6 @@ model project_bandwidth_rollup (
field egress_allocated uint64 ( updatable )
)
read scalar (
select project_bandwidth_rollup
where project_bandwidth_rollup.project_id = ?
where project_bandwidth_rollup.interval_month = ?
)
model bucket_storage_tally (
key bucket_name project_id interval_start
@ -564,12 +504,6 @@ model bucket_storage_tally (
create bucket_storage_tally ( noreturn )
read first (
select bucket_storage_tally
where bucket_storage_tally.project_id = ?
orderby desc bucket_storage_tally.interval_start
)
read all (
select bucket_storage_tally
)
@ -661,8 +595,6 @@ model storagenode_bandwidth_rollup_phase2 (
field settled uint64 ( updatable )
)
create storagenode_bandwidth_rollup_phase2 ( )
read paged (
select storagenode_bandwidth_rollup_phase2
where storagenode_bandwidth_rollup_phase2.storagenode_id = ?
@ -734,7 +666,7 @@ model storagenode_paystub (
field distributed int64 // in micro-units of currency
)
create storagenode_paystub ( noreturn )
create storagenode_paystub ( noreturn, replace )
read one (
select storagenode_paystub
@ -775,6 +707,11 @@ read all (
where storagenode_payment.node_id = ?
)
read all (
select storagenode_payment
where storagenode_payment.node_id = ?
where storagenode_payment.period = ?
)
//--- peer_identity ---//
@ -877,23 +814,6 @@ model offer (
field type int ( updatable )
)
read one (
select offer
where offer.id = ?
)
read all (
select offer
orderby asc offer.id
)
update offer (
where offer.id = ?
noreturn
)
create offer ( )
//--- user credit table ---//
@ -920,21 +840,6 @@ model user_credit (
field created_at timestamp ( autoinsert )
)
read all (
select user_credit
where user_credit.user_id = ?
where user_credit.expires_at > ?
where user_credit.credits_used_in_cents < user_credit.credits_earned_in_cents
orderby asc user_credit.expires_at
)
read count (
select user_credit
where user_credit.referred_by = ?
)
create user_credit ()
//--- metainfo buckets ---//
model bucket_metainfo (
@ -1018,12 +923,6 @@ model graceful_exit_progress (
field updated_at timestamp ( autoinsert, autoupdate )
)
create graceful_exit_progress ( noreturn )
update graceful_exit_progress (
where graceful_exit_progress.node_id = ?
noreturn
)
delete graceful_exit_progress ( where graceful_exit_progress.node_id = ? )
read one (
select graceful_exit_progress
where graceful_exit_progress.node_id = ?
@ -1054,7 +953,6 @@ model graceful_exit_transfer_queue (
)
)
create graceful_exit_transfer_queue ( noreturn )
update graceful_exit_transfer_queue (
where graceful_exit_transfer_queue.node_id = ?
where graceful_exit_transfer_queue.path = ?
@ -1129,12 +1027,6 @@ read all (
where coinpayments_transaction.user_id = ?
orderby desc coinpayments_transaction.created_at
)
read limitoffset (
select coinpayments_transaction
where coinpayments_transaction.created_at <= ?
where coinpayments_transaction.status = ?
orderby desc coinpayments_transaction.created_at
)
model stripecoinpayments_apply_balance_intent (
key tx_id
@ -1145,14 +1037,6 @@ model stripecoinpayments_apply_balance_intent (
field created_at timestamp ( autoinsert )
)
create stripecoinpayments_apply_balance_intent ()
update stripecoinpayments_apply_balance_intent (
where stripecoinpayments_apply_balance_intent.tx_id = ?
)
delete stripecoinpayments_apply_balance_intent (
where stripecoinpayments_apply_balance_intent.tx_id = ?
)
model stripecoinpayments_invoice_project_record (
key id
@ -1174,10 +1058,6 @@ create stripecoinpayments_invoice_project_record ()
update stripecoinpayments_invoice_project_record (
where stripecoinpayments_invoice_project_record.id = ?
)
delete stripecoinpayments_invoice_project_record (
where stripecoinpayments_invoice_project_record.id = ?
)
read one (
select stripecoinpayments_invoice_project_record
where stripecoinpayments_invoice_project_record.project_id = ?

File diff suppressed because it is too large Load Diff

View File

@ -163,8 +163,6 @@ CREATE TABLE nodes (
audit_reputation_beta double precision NOT NULL DEFAULT 0,
unknown_audit_reputation_alpha double precision NOT NULL DEFAULT 1,
unknown_audit_reputation_beta double precision NOT NULL DEFAULT 0,
uptime_reputation_alpha double precision NOT NULL DEFAULT 1,
uptime_reputation_beta double precision NOT NULL DEFAULT 0,
exit_initiated_at timestamp with time zone,
exit_loop_completed_at timestamp with time zone,
exit_finished_at timestamp with time zone,

View File

@ -163,8 +163,6 @@ CREATE TABLE nodes (
audit_reputation_beta double precision NOT NULL DEFAULT 0,
unknown_audit_reputation_alpha double precision NOT NULL DEFAULT 1,
unknown_audit_reputation_beta double precision NOT NULL DEFAULT 0,
uptime_reputation_alpha double precision NOT NULL DEFAULT 1,
uptime_reputation_beta double precision NOT NULL DEFAULT 0,
exit_initiated_at timestamp with time zone,
exit_loop_completed_at timestamp with time zone,
exit_finished_at timestamp with time zone,

View File

@ -25,6 +25,10 @@ type gracefulexitDB struct {
db *satelliteDB
}
const (
deleteExitProgressBatchSize = 1000
)
// IncrementProgress increments transfer stats for a node.
func (db *gracefulexitDB) IncrementProgress(ctx context.Context, nodeID storj.NodeID, bytes int64, successfulTransfers int64, failedTransfers int64) (err error) {
defer mon.Task()(&ctx)(&err)
@ -183,6 +187,74 @@ func (db *gracefulexitDB) DeleteAllFinishedTransferQueueItems(
return count, nil
}
// DeleteFinishedExitProgress deletes exit progress entries for nodes that
// finished exiting before the indicated time, returns number of deleted entries.
func (db *gracefulexitDB) DeleteFinishedExitProgress(
ctx context.Context, before time.Time) (_ int64, err error) {
defer mon.Task()(&ctx)(&err)
finishedNodes, err := db.GetFinishedExitNodes(ctx, before)
if err != nil {
return 0, err
}
return db.DeleteBatchExitProgress(ctx, finishedNodes)
}
// GetFinishedExitNodes gets nodes that are marked having finished graceful exit before a given time.
func (db *gracefulexitDB) GetFinishedExitNodes(ctx context.Context, before time.Time) (finishedNodes []storj.NodeID, err error) {
defer mon.Task()(&ctx)(&err)
stmt := db.db.Rebind(
` SELECT id FROM nodes
WHERE exit_finished_at IS NOT NULL
AND exit_finished_at < ?`,
)
rows, err := db.db.Query(ctx, stmt, before.UTC())
if err != nil {
return nil, Error.Wrap(err)
}
defer func() {
err = Error.Wrap(errs.Combine(err, rows.Close()))
}()
for rows.Next() {
var id storj.NodeID
err = rows.Scan(&id)
if err != nil {
return nil, Error.Wrap(err)
}
finishedNodes = append(finishedNodes, id)
}
return finishedNodes, Error.Wrap(rows.Err())
}
// DeleteBatchExitProgress batch deletes from exit progress. This is separate from
// getting the node IDs because the combined query is slow in CRDB. It's safe to do
// separately because if nodes are deleted between the get and delete, it doesn't
// affect correctness.
func (db *gracefulexitDB) DeleteBatchExitProgress(ctx context.Context, nodeIDs []storj.NodeID) (deleted int64, err error) {
defer mon.Task()(&ctx)(&err)
stmt := `DELETE from graceful_exit_progress
WHERE node_id = ANY($1)`
for len(nodeIDs) > 0 {
numToSubmit := len(nodeIDs)
if numToSubmit > deleteExitProgressBatchSize {
numToSubmit = deleteExitProgressBatchSize
}
nodesToSubmit := nodeIDs[:numToSubmit]
res, err := db.db.ExecContext(ctx, stmt, pgutil.NodeIDArray(nodesToSubmit))
if err != nil {
return deleted, Error.Wrap(err)
}
count, err := res.RowsAffected()
if err != nil {
return deleted, Error.Wrap(err)
}
deleted += count
nodeIDs = nodeIDs[numToSubmit:]
}
return deleted, Error.Wrap(err)
}
// GetTransferQueueItem gets a graceful exit transfer queue entry.
func (db *gracefulexitDB) GetTransferQueueItem(ctx context.Context, nodeID storj.NodeID, key metabase.SegmentKey, pieceNum int32) (_ *gracefulexit.TransferQueueItem, err error) {
defer mon.Task()(&ctx)(&err)

View File

@ -1265,6 +1265,15 @@ func (db *satelliteDB) PostgresMigration() *migrate.Migration {
`ALTER TABLE users ADD COLUMN employee_count text;`,
},
},
{
DB: &db.migrationDB,
Description: "drop unused columns uptime_reputation_alpha and uptime_reputation_beta from nodes table",
Version: 148,
Action: migrate.SQL{
`ALTER TABLE nodes DROP COLUMN uptime_reputation_alpha;`,
`ALTER TABLE nodes DROP COLUMN uptime_reputation_beta;`,
},
},
// NB: after updating testdata in `testdata`, run
// `go generate` to update `migratez.go`.
},

View File

@ -13,7 +13,7 @@ func (db *satelliteDB) testMigration() *migrate.Migration {
{
DB: &db.migrationDB,
Description: "Testing setup",
Version: 147,
Version: 148,
Action: migrate.SQL{`-- AUTOGENERATED BY storj.io/dbx
-- DO NOT EDIT
CREATE TABLE accounting_rollups (
@ -141,51 +141,49 @@ CREATE TABLE irreparabledbs (
PRIMARY KEY ( segmentpath )
);
CREATE TABLE nodes (
id bytea NOT NULL,
address text NOT NULL DEFAULT '',
last_net text NOT NULL,
last_ip_port text,
protocol integer NOT NULL DEFAULT 0,
type integer NOT NULL DEFAULT 0,
email text NOT NULL,
wallet text NOT NULL,
wallet_features text NOT NULL DEFAULT '',
free_disk bigint NOT NULL DEFAULT -1,
piece_count bigint NOT NULL DEFAULT 0,
major bigint NOT NULL DEFAULT 0,
minor bigint NOT NULL DEFAULT 0,
patch bigint NOT NULL DEFAULT 0,
hash text NOT NULL DEFAULT '',
timestamp timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00+00',
release boolean NOT NULL DEFAULT false,
latency_90 bigint NOT NULL DEFAULT 0,
audit_success_count bigint NOT NULL DEFAULT 0,
total_audit_count bigint NOT NULL DEFAULT 0,
vetted_at timestamp with time zone,
uptime_success_count bigint NOT NULL DEFAULT 0,
total_uptime_count bigint NOT NULL DEFAULT 0,
created_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
updated_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
last_contact_success timestamp with time zone NOT NULL DEFAULT 'epoch',
last_contact_failure timestamp with time zone NOT NULL DEFAULT 'epoch',
contained boolean NOT NULL DEFAULT false,
disqualified timestamp with time zone,
suspended timestamp with time zone,
unknown_audit_suspended timestamp with time zone,
offline_suspended timestamp with time zone,
under_review timestamp with time zone,
online_score double precision NOT NULL DEFAULT 1,
audit_reputation_alpha double precision NOT NULL DEFAULT 1,
audit_reputation_beta double precision NOT NULL DEFAULT 0,
unknown_audit_reputation_alpha double precision NOT NULL DEFAULT 1,
unknown_audit_reputation_beta double precision NOT NULL DEFAULT 0,
uptime_reputation_alpha double precision NOT NULL DEFAULT 1,
uptime_reputation_beta double precision NOT NULL DEFAULT 0,
exit_initiated_at timestamp with time zone,
exit_loop_completed_at timestamp with time zone,
exit_finished_at timestamp with time zone,
exit_success boolean NOT NULL DEFAULT false,
PRIMARY KEY ( id )
id bytea NOT NULL,
address text NOT NULL DEFAULT '',
last_net text NOT NULL,
last_ip_port text,
protocol integer NOT NULL DEFAULT 0,
type integer NOT NULL DEFAULT 0,
email text NOT NULL,
wallet text NOT NULL,
wallet_features text NOT NULL DEFAULT '',
free_disk bigint NOT NULL DEFAULT -1,
piece_count bigint NOT NULL DEFAULT 0,
major bigint NOT NULL DEFAULT 0,
minor bigint NOT NULL DEFAULT 0,
patch bigint NOT NULL DEFAULT 0,
hash text NOT NULL DEFAULT '',
timestamp timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00+00',
release boolean NOT NULL DEFAULT false,
latency_90 bigint NOT NULL DEFAULT 0,
audit_success_count bigint NOT NULL DEFAULT 0,
total_audit_count bigint NOT NULL DEFAULT 0,
vetted_at timestamp with time zone,
uptime_success_count bigint NOT NULL DEFAULT 0,
total_uptime_count bigint NOT NULL DEFAULT 0,
created_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
updated_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
last_contact_success timestamp with time zone NOT NULL DEFAULT 'epoch',
last_contact_failure timestamp with time zone NOT NULL DEFAULT 'epoch',
contained boolean NOT NULL DEFAULT false,
disqualified timestamp with time zone,
suspended timestamp with time zone,
unknown_audit_suspended timestamp with time zone,
offline_suspended timestamp with time zone,
under_review timestamp with time zone,
online_score double precision NOT NULL DEFAULT 1,
audit_reputation_alpha double precision NOT NULL DEFAULT 1,
audit_reputation_beta double precision NOT NULL DEFAULT 0,
unknown_audit_reputation_alpha double precision NOT NULL DEFAULT 1,
unknown_audit_reputation_beta double precision NOT NULL DEFAULT 0,
exit_initiated_at timestamp with time zone,
exit_loop_completed_at timestamp with time zone,
exit_finished_at timestamp with time zone,
exit_success boolean NOT NULL DEFAULT false,
PRIMARY KEY ( id )
);
CREATE TABLE node_api_versions (
id bytea NOT NULL,
@ -372,10 +370,10 @@ CREATE TABLE users (
project_limit integer NOT NULL DEFAULT 0,
position text,
company_name text,
company_size int,
company_size integer,
working_on text,
is_professional boolean NOT NULL DEFAULT false,
employee_count text,
employee_count text,
PRIMARY KEY ( id )
);
CREATE TABLE value_attributions (
@ -460,6 +458,7 @@ CREATE INDEX storagenode_paystubs_node_id_index ON storagenode_paystubs ( node_i
CREATE INDEX storagenode_storage_tallies_node_id_index ON storagenode_storage_tallies ( node_id );
CREATE UNIQUE INDEX credits_earned_user_id_offer_id ON user_credits ( id, offer_id );
INSERT INTO "offers" ("id", "name", "description", "award_credit_in_cents", "invitee_credit_in_cents", "expires_at", "created_at", "status", "type", "award_credit_duration_days", "invitee_credit_duration_days") VALUES (1, 'Default referral offer', 'Is active when no other active referral offer', 300, 600, '2119-03-14 08:28:24.636949+00', '2019-07-14 08:28:24.636949+00', 1, 2, 365, 14);
INSERT INTO "offers" ("id", "name", "description", "award_credit_in_cents", "invitee_credit_in_cents", "expires_at", "created_at", "status", "type", "award_credit_duration_days", "invitee_credit_duration_days") VALUES (2, 'Default free credit offer', 'Is active when no active free credit offer', 0, 300, '2119-03-14 08:28:24.636949+00', '2019-07-14 08:28:24.636949+00', 1, 1, NULL, 14);

View File

@ -1561,7 +1561,15 @@ func (cache *overlaycache) DQNodesLastSeenBefore(ctx context.Context, cutoff tim
WHERE last_contact_success < $1
AND disqualified is NULL
AND exit_finished_at is NULL;`
_, err = cache.db.ExecContext(ctx, q, cutoff)
results, err := cache.db.ExecContext(ctx, q, cutoff)
if err != nil {
return err
}
n, err := results.RowsAffected()
if err != nil {
return err
}
mon.IntVal("stray_nodes_dq_count").Observe(n)
return err
}

View File

@ -155,7 +155,7 @@ func derefStringOr(v *string, def string) string {
// TestCreatePaystub inserts storagenode_paystub into database. Only used for tests.
func (db *snopayoutsDB) TestCreatePaystub(ctx context.Context, stub snopayouts.Paystub) (err error) {
return db.db.CreateNoReturn_StoragenodePaystub(ctx,
return db.db.ReplaceNoReturn_StoragenodePaystub(ctx,
dbx.StoragenodePaystub_Period(stub.Period),
dbx.StoragenodePaystub_NodeId(stub.NodeID.Bytes()),
dbx.StoragenodePaystub_Codes(stub.Codes),

View File

@ -0,0 +1,563 @@
-- AUTOGENERATED BY storj.io/dbx
-- DO NOT EDIT
CREATE TABLE accounting_rollups (
node_id bytea NOT NULL,
start_time timestamp with time zone NOT NULL,
put_total bigint NOT NULL,
get_total bigint NOT NULL,
get_audit_total bigint NOT NULL,
get_repair_total bigint NOT NULL,
put_repair_total bigint NOT NULL,
at_rest_total double precision NOT NULL,
PRIMARY KEY ( node_id, start_time )
);
CREATE TABLE accounting_timestamps (
name text NOT NULL,
value timestamp with time zone NOT NULL,
PRIMARY KEY ( name )
);
CREATE TABLE audit_histories (
node_id bytea NOT NULL,
history bytea NOT NULL,
PRIMARY KEY ( node_id )
);
CREATE TABLE bucket_bandwidth_rollups (
bucket_name bytea NOT NULL,
project_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
interval_seconds integer NOT NULL,
action integer NOT NULL,
inline bigint NOT NULL,
allocated bigint NOT NULL,
settled bigint NOT NULL,
PRIMARY KEY ( bucket_name, project_id, interval_start, action )
);
CREATE TABLE bucket_bandwidth_rollup_archives (
bucket_name bytea NOT NULL,
project_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
interval_seconds integer NOT NULL,
action integer NOT NULL,
inline bigint NOT NULL,
allocated bigint NOT NULL,
settled bigint NOT NULL,
PRIMARY KEY ( bucket_name, project_id, interval_start, action )
);
CREATE TABLE bucket_storage_tallies (
bucket_name bytea NOT NULL,
project_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
inline bigint NOT NULL,
remote bigint NOT NULL,
remote_segments_count integer NOT NULL,
inline_segments_count integer NOT NULL,
object_count integer NOT NULL,
metadata_size bigint NOT NULL,
PRIMARY KEY ( bucket_name, project_id, interval_start )
);
CREATE TABLE coinpayments_transactions (
id text NOT NULL,
user_id bytea NOT NULL,
address text NOT NULL,
amount bytea NOT NULL,
received bytea NOT NULL,
status integer NOT NULL,
key text NOT NULL,
timeout integer NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id )
);
CREATE TABLE coupons (
id bytea NOT NULL,
user_id bytea NOT NULL,
amount bigint NOT NULL,
description text NOT NULL,
type integer NOT NULL,
status integer NOT NULL,
duration bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id )
);
CREATE TABLE coupon_usages (
coupon_id bytea NOT NULL,
amount bigint NOT NULL,
status integer NOT NULL,
period timestamp with time zone NOT NULL,
PRIMARY KEY ( coupon_id, period )
);
CREATE TABLE graceful_exit_progress (
node_id bytea NOT NULL,
bytes_transferred bigint NOT NULL,
pieces_transferred bigint NOT NULL DEFAULT 0,
pieces_failed bigint NOT NULL DEFAULT 0,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY ( node_id )
);
CREATE TABLE graceful_exit_transfer_queue (
node_id bytea NOT NULL,
path bytea NOT NULL,
piece_num integer NOT NULL,
root_piece_id bytea,
durability_ratio double precision NOT NULL,
queued_at timestamp with time zone NOT NULL,
requested_at timestamp with time zone,
last_failed_at timestamp with time zone,
last_failed_code integer,
failed_count integer,
finished_at timestamp with time zone,
order_limit_send_count integer NOT NULL DEFAULT 0,
PRIMARY KEY ( node_id, path, piece_num )
);
CREATE TABLE injuredsegments (
path bytea NOT NULL,
data bytea NOT NULL,
attempted timestamp with time zone,
updated_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
segment_health double precision NOT NULL DEFAULT 1,
PRIMARY KEY ( path )
);
CREATE TABLE irreparabledbs (
segmentpath bytea NOT NULL,
segmentdetail bytea NOT NULL,
pieces_lost_count bigint NOT NULL,
seg_damaged_unix_sec bigint NOT NULL,
repair_attempt_count bigint NOT NULL,
PRIMARY KEY ( segmentpath )
);
CREATE TABLE nodes (
id bytea NOT NULL,
address text NOT NULL DEFAULT '',
last_net text NOT NULL,
last_ip_port text,
protocol integer NOT NULL DEFAULT 0,
type integer NOT NULL DEFAULT 0,
email text NOT NULL,
wallet text NOT NULL,
wallet_features text NOT NULL DEFAULT '',
free_disk bigint NOT NULL DEFAULT -1,
piece_count bigint NOT NULL DEFAULT 0,
major bigint NOT NULL DEFAULT 0,
minor bigint NOT NULL DEFAULT 0,
patch bigint NOT NULL DEFAULT 0,
hash text NOT NULL DEFAULT '',
timestamp timestamp with time zone NOT NULL DEFAULT '0001-01-01 00:00:00+00',
release boolean NOT NULL DEFAULT false,
latency_90 bigint NOT NULL DEFAULT 0,
audit_success_count bigint NOT NULL DEFAULT 0,
total_audit_count bigint NOT NULL DEFAULT 0,
vetted_at timestamp with time zone,
uptime_success_count bigint NOT NULL DEFAULT 0,
total_uptime_count bigint NOT NULL DEFAULT 0,
created_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
updated_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
last_contact_success timestamp with time zone NOT NULL DEFAULT 'epoch',
last_contact_failure timestamp with time zone NOT NULL DEFAULT 'epoch',
contained boolean NOT NULL DEFAULT false,
disqualified timestamp with time zone,
suspended timestamp with time zone,
unknown_audit_suspended timestamp with time zone,
offline_suspended timestamp with time zone,
under_review timestamp with time zone,
online_score double precision NOT NULL DEFAULT 1,
audit_reputation_alpha double precision NOT NULL DEFAULT 1,
audit_reputation_beta double precision NOT NULL DEFAULT 0,
unknown_audit_reputation_alpha double precision NOT NULL DEFAULT 1,
unknown_audit_reputation_beta double precision NOT NULL DEFAULT 0,
exit_initiated_at timestamp with time zone,
exit_loop_completed_at timestamp with time zone,
exit_finished_at timestamp with time zone,
exit_success boolean NOT NULL DEFAULT false,
PRIMARY KEY ( id )
);
CREATE TABLE node_api_versions (
id bytea NOT NULL,
api_version integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id )
);
CREATE TABLE offers (
id serial NOT NULL,
name text NOT NULL,
description text NOT NULL,
award_credit_in_cents integer NOT NULL DEFAULT 0,
invitee_credit_in_cents integer NOT NULL DEFAULT 0,
award_credit_duration_days integer,
invitee_credit_duration_days integer,
redeemable_cap integer,
expires_at timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
status integer NOT NULL,
type integer NOT NULL,
PRIMARY KEY ( id )
);
CREATE TABLE peer_identities (
node_id bytea NOT NULL,
leaf_serial_number bytea NOT NULL,
chain bytea NOT NULL,
updated_at timestamp with time zone NOT NULL,
PRIMARY KEY ( node_id )
);
CREATE TABLE pending_audits (
node_id bytea NOT NULL,
piece_id bytea NOT NULL,
stripe_index bigint NOT NULL,
share_size bigint NOT NULL,
expected_share_hash bytea NOT NULL,
reverify_count bigint NOT NULL,
path bytea NOT NULL,
PRIMARY KEY ( node_id )
);
CREATE TABLE projects (
id bytea NOT NULL,
name text NOT NULL,
description text NOT NULL,
usage_limit bigint,
bandwidth_limit bigint,
rate_limit integer,
max_buckets integer,
partner_id bytea,
owner_id bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id )
);
CREATE TABLE project_bandwidth_rollups (
project_id bytea NOT NULL,
interval_month date NOT NULL,
egress_allocated bigint NOT NULL,
PRIMARY KEY ( project_id, interval_month )
);
CREATE TABLE registration_tokens (
secret bytea NOT NULL,
owner_id bytea,
project_limit integer NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( secret ),
UNIQUE ( owner_id )
);
CREATE TABLE reset_password_tokens (
secret bytea NOT NULL,
owner_id bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( secret ),
UNIQUE ( owner_id )
);
CREATE TABLE revocations (
revoked bytea NOT NULL,
api_key_id bytea NOT NULL,
PRIMARY KEY ( revoked )
);
CREATE TABLE storagenode_bandwidth_rollups (
storagenode_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
interval_seconds integer NOT NULL,
action integer NOT NULL,
allocated bigint DEFAULT 0,
settled bigint NOT NULL,
PRIMARY KEY ( storagenode_id, interval_start, action )
);
CREATE TABLE storagenode_bandwidth_rollup_archives (
storagenode_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
interval_seconds integer NOT NULL,
action integer NOT NULL,
allocated bigint DEFAULT 0,
settled bigint NOT NULL,
PRIMARY KEY ( storagenode_id, interval_start, action )
);
CREATE TABLE storagenode_bandwidth_rollups_phase2 (
storagenode_id bytea NOT NULL,
interval_start timestamp with time zone NOT NULL,
interval_seconds integer NOT NULL,
action integer NOT NULL,
allocated bigint DEFAULT 0,
settled bigint NOT NULL,
PRIMARY KEY ( storagenode_id, interval_start, action )
);
CREATE TABLE storagenode_payments (
id bigserial NOT NULL,
created_at timestamp with time zone NOT NULL,
node_id bytea NOT NULL,
period text NOT NULL,
amount bigint NOT NULL,
receipt text,
notes text,
PRIMARY KEY ( id )
);
CREATE TABLE storagenode_paystubs (
period text NOT NULL,
node_id bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
codes text NOT NULL,
usage_at_rest double precision NOT NULL,
usage_get bigint NOT NULL,
usage_put bigint NOT NULL,
usage_get_repair bigint NOT NULL,
usage_put_repair bigint NOT NULL,
usage_get_audit bigint NOT NULL,
comp_at_rest bigint NOT NULL,
comp_get bigint NOT NULL,
comp_put bigint NOT NULL,
comp_get_repair bigint NOT NULL,
comp_put_repair bigint NOT NULL,
comp_get_audit bigint NOT NULL,
surge_percent bigint NOT NULL,
held bigint NOT NULL,
owed bigint NOT NULL,
disposed bigint NOT NULL,
paid bigint NOT NULL,
distributed bigint NOT NULL,
PRIMARY KEY ( period, node_id )
);
CREATE TABLE storagenode_storage_tallies (
node_id bytea NOT NULL,
interval_end_time timestamp with time zone NOT NULL,
data_total double precision NOT NULL,
PRIMARY KEY ( interval_end_time, node_id )
);
CREATE TABLE stripe_customers (
user_id bytea NOT NULL,
customer_id text NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( user_id ),
UNIQUE ( customer_id )
);
CREATE TABLE stripecoinpayments_invoice_project_records (
id bytea NOT NULL,
project_id bytea NOT NULL,
storage double precision NOT NULL,
egress bigint NOT NULL,
objects bigint NOT NULL,
period_start timestamp with time zone NOT NULL,
period_end timestamp with time zone NOT NULL,
state integer NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id ),
UNIQUE ( project_id, period_start, period_end )
);
CREATE TABLE stripecoinpayments_tx_conversion_rates (
tx_id text NOT NULL,
rate bytea NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( tx_id )
);
CREATE TABLE users (
id bytea NOT NULL,
email text NOT NULL,
normalized_email text NOT NULL,
full_name text NOT NULL,
short_name text,
password_hash bytea NOT NULL,
status integer NOT NULL,
partner_id bytea,
created_at timestamp with time zone NOT NULL,
project_limit integer NOT NULL DEFAULT 0,
position text,
company_name text,
company_size integer,
working_on text,
is_professional boolean NOT NULL DEFAULT false,
employee_count text,
PRIMARY KEY ( id )
);
CREATE TABLE value_attributions (
project_id bytea NOT NULL,
bucket_name bytea NOT NULL,
partner_id bytea NOT NULL,
last_updated timestamp with time zone NOT NULL,
PRIMARY KEY ( project_id, bucket_name )
);
CREATE TABLE api_keys (
id bytea NOT NULL,
project_id bytea NOT NULL REFERENCES projects( id ) ON DELETE CASCADE,
head bytea NOT NULL,
name text NOT NULL,
secret bytea NOT NULL,
partner_id bytea,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id ),
UNIQUE ( head ),
UNIQUE ( name, project_id )
);
CREATE TABLE bucket_metainfos (
id bytea NOT NULL,
project_id bytea NOT NULL REFERENCES projects( id ),
name bytea NOT NULL,
partner_id bytea,
path_cipher integer NOT NULL,
created_at timestamp with time zone NOT NULL,
default_segment_size integer NOT NULL,
default_encryption_cipher_suite integer NOT NULL,
default_encryption_block_size integer NOT NULL,
default_redundancy_algorithm integer NOT NULL,
default_redundancy_share_size integer NOT NULL,
default_redundancy_required_shares integer NOT NULL,
default_redundancy_repair_shares integer NOT NULL,
default_redundancy_optimal_shares integer NOT NULL,
default_redundancy_total_shares integer NOT NULL,
PRIMARY KEY ( id ),
UNIQUE ( project_id, name )
);
CREATE TABLE project_members (
member_id bytea NOT NULL REFERENCES users( id ) ON DELETE CASCADE,
project_id bytea NOT NULL REFERENCES projects( id ) ON DELETE CASCADE,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( member_id, project_id )
);
CREATE TABLE stripecoinpayments_apply_balance_intents (
tx_id text NOT NULL REFERENCES coinpayments_transactions( id ) ON DELETE CASCADE,
state integer NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( tx_id )
);
CREATE TABLE user_credits (
id serial NOT NULL,
user_id bytea NOT NULL REFERENCES users( id ) ON DELETE CASCADE,
offer_id integer NOT NULL REFERENCES offers( id ),
referred_by bytea REFERENCES users( id ) ON DELETE SET NULL,
type text NOT NULL,
credits_earned_in_cents integer NOT NULL,
credits_used_in_cents integer NOT NULL,
expires_at timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
PRIMARY KEY ( id ),
UNIQUE ( id, offer_id )
);
CREATE INDEX accounting_rollups_start_time_index ON accounting_rollups ( start_time );
CREATE INDEX bucket_bandwidth_rollups_project_id_action_interval_index ON bucket_bandwidth_rollups ( project_id, action, interval_start );
CREATE INDEX bucket_bandwidth_rollups_action_interval_project_id_index ON bucket_bandwidth_rollups ( action, interval_start, project_id );
CREATE INDEX bucket_bandwidth_rollups_archive_project_id_action_interval_index ON bucket_bandwidth_rollup_archives ( project_id, action, interval_start );
CREATE INDEX bucket_bandwidth_rollups_archive_action_interval_project_id_index ON bucket_bandwidth_rollup_archives ( action, interval_start, project_id );
CREATE INDEX bucket_storage_tallies_project_id_interval_start_index ON bucket_storage_tallies ( project_id, interval_start );
CREATE INDEX graceful_exit_transfer_queue_nid_dr_qa_fa_lfa_index ON graceful_exit_transfer_queue ( node_id, durability_ratio, queued_at, finished_at, last_failed_at );
CREATE INDEX injuredsegments_attempted_index ON injuredsegments ( attempted );
CREATE INDEX injuredsegments_segment_health_index ON injuredsegments ( segment_health );
CREATE INDEX injuredsegments_updated_at_index ON injuredsegments ( updated_at );
CREATE INDEX node_last_ip ON nodes ( last_net );
CREATE INDEX nodes_dis_unk_exit_fin_last_success_index ON nodes ( disqualified, unknown_audit_suspended, exit_finished_at, last_contact_success );
CREATE INDEX storagenode_bandwidth_rollups_interval_start_index ON storagenode_bandwidth_rollups ( interval_start );
CREATE INDEX storagenode_bandwidth_rollup_archives_interval_start_index ON storagenode_bandwidth_rollup_archives ( interval_start );
CREATE INDEX storagenode_payments_node_id_period_index ON storagenode_payments ( node_id, period );
CREATE INDEX storagenode_paystubs_node_id_index ON storagenode_paystubs ( node_id );
CREATE INDEX storagenode_storage_tallies_node_id_index ON storagenode_storage_tallies ( node_id );
CREATE UNIQUE INDEX credits_earned_user_id_offer_id ON user_credits ( id, offer_id );
INSERT INTO "offers" ("id", "name", "description", "award_credit_in_cents", "invitee_credit_in_cents", "expires_at", "created_at", "status", "type", "award_credit_duration_days", "invitee_credit_duration_days") VALUES (1, 'Default referral offer', 'Is active when no other active referral offer', 300, 600, '2119-03-14 08:28:24.636949+00', '2019-07-14 08:28:24.636949+00', 1, 2, 365, 14);
INSERT INTO "offers" ("id", "name", "description", "award_credit_in_cents", "invitee_credit_in_cents", "expires_at", "created_at", "status", "type", "award_credit_duration_days", "invitee_credit_duration_days") VALUES (2, 'Default free credit offer', 'Is active when no active free credit offer', 0, 300, '2119-03-14 08:28:24.636949+00', '2019-07-14 08:28:24.636949+00', 1, 1, NULL, 14);
-- MAIN DATA --
INSERT INTO "accounting_rollups"("node_id", "start_time", "put_total", "get_total", "get_audit_total", "get_repair_total", "put_repair_total", "at_rest_total") VALUES (E'\\367M\\177\\251]t/\\022\\256\\214\\265\\025\\224\\204:\\217\\212\\0102<\\321\\374\\020&\\271Qc\\325\\261\\354\\246\\233'::bytea, '2019-02-09 00:00:00+00', 3000, 6000, 9000, 12000, 0, 15000);
INSERT INTO "accounting_timestamps" VALUES ('LastAtRestTally', '0001-01-01 00:00:00+00');
INSERT INTO "accounting_timestamps" VALUES ('LastRollup', '0001-01-01 00:00:00+00');
INSERT INTO "accounting_timestamps" VALUES ('LastBandwidthTally', '0001-01-01 00:00:00+00');
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\153\\313\\233\\074\\327\\177\\136\\070\\346\\001', '127.0.0.1:55516', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', '127.0.0.1:55518', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 0, 3, 3, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014', '127.0.0.1:55517', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 0, 0, 0, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\015', '127.0.0.1:55519', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 1, 2, 1, 2, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "vetted_at", "online_score") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', '127.0.0.1:55520', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 300, 400, 300, 400, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 300, 0, 1, 0, false, '2020-03-18 12:00:00.000000+00', 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\154\\313\\233\\074\\327\\177\\136\\070\\346\\001', '127.0.0.1:55516', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 75, 25, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "last_ip_port", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\154\\313\\233\\074\\327\\177\\136\\070\\346\\002', '127.0.0.1:55516', '127.0.0.0', '127.0.0.1:55516', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 75, 25, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\363\\341\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', '127.0.0.1:55516', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "wallet_features", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "online_score") VALUES (E'\\362\\341\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', '127.0.0.1:55516', '', 0, 4, '', '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, 1);
INSERT INTO "users"("id", "full_name", "short_name", "email", "normalized_email", "password_hash", "status", "partner_id", "created_at", "is_professional") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 'Noahson', 'William', '1email1@mail.test', '1EMAIL1@MAIL.TEST', E'some_readable_hash'::bytea, 1, NULL, '2019-02-14 08:28:24.614594+00', false);
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "max_buckets", "partner_id", "owner_id", "created_at") VALUES (E'\\022\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, 'ProjectName', 'projects description', NULL, NULL, NULL, NULL, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2019-02-14 08:28:24.254934+00');
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "max_buckets", "partner_id", "owner_id", "created_at") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea, 'projName1', 'Test project 1', NULL, NULL, NULL, NULL, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2019-02-14 08:28:24.636949+00');
INSERT INTO "project_members"("member_id", "project_id", "created_at") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea, '2019-02-14 08:28:24.677953+00');
INSERT INTO "project_members"("member_id", "project_id", "created_at") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, E'\\022\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, '2019-02-13 08:28:24.677953+00');
INSERT INTO "irreparabledbs" ("segmentpath", "segmentdetail", "pieces_lost_count", "seg_damaged_unix_sec", "repair_attempt_count") VALUES ('\x49616d5365676d656e746b6579696e666f30', '\x49616d5365676d656e7464657461696c696e666f30', 10, 1550159554, 10);
INSERT INTO "registration_tokens" ("secret", "owner_id", "project_limit", "created_at") VALUES (E'\\070\\127\\144\\013\\332\\344\\102\\376\\306\\056\\303\\130\\106\\132\\321\\276\\321\\274\\170\\264\\054\\333\\221\\116\\154\\221\\335\\070\\220\\146\\344\\216'::bytea, null, 1, '2019-02-14 08:28:24.677953+00');
INSERT INTO "storagenode_bandwidth_rollups" ("storagenode_id", "interval_start", "interval_seconds", "action", "allocated", "settled") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', '2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024);
INSERT INTO "storagenode_storage_tallies" VALUES (E'\\3510\\323\\225"~\\036<\\342\\330m\\0253Jhr\\246\\233K\\246#\\2303\\351\\256\\275j\\212UM\\362\\207', '2019-02-14 08:16:57.812849+00', 1000);
INSERT INTO "bucket_bandwidth_rollups" ("bucket_name", "project_id", "interval_start", "interval_seconds", "action", "inline", "allocated", "settled") VALUES (E'testbucket'::bytea, E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea,'2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024, 3024);
INSERT INTO "bucket_storage_tallies" ("bucket_name", "project_id", "interval_start", "inline", "remote", "remote_segments_count", "inline_segments_count", "object_count", "metadata_size") VALUES (E'testbucket'::bytea, E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea,'2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 4024, 5024, 0, 0, 0, 0);
INSERT INTO "bucket_bandwidth_rollups" ("bucket_name", "project_id", "interval_start", "interval_seconds", "action", "inline", "allocated", "settled") VALUES (E'testbucket'::bytea, E'\\170\\160\\157\\370\\274\\366\\113\\364\\272\\235\\301\\243\\321\\102\\321\\136'::bytea,'2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024, 3024);
INSERT INTO "bucket_storage_tallies" ("bucket_name", "project_id", "interval_start", "inline", "remote", "remote_segments_count", "inline_segments_count", "object_count", "metadata_size") VALUES (E'testbucket'::bytea, E'\\170\\160\\157\\370\\274\\366\\113\\364\\272\\235\\301\\243\\321\\102\\321\\136'::bytea,'2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 4024, 5024, 0, 0, 0, 0);
INSERT INTO "reset_password_tokens" ("secret", "owner_id", "created_at") VALUES (E'\\070\\127\\144\\013\\332\\344\\102\\376\\306\\056\\303\\130\\106\\132\\321\\276\\321\\274\\170\\264\\054\\333\\221\\116\\154\\221\\335\\070\\220\\146\\344\\216'::bytea, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2019-05-08 08:28:24.677953+00');
INSERT INTO "api_keys" ("id", "project_id", "head", "name", "secret", "partner_id", "created_at") VALUES (E'\\334/\\302;\\225\\355O\\323\\276f\\247\\354/6\\241\\033'::bytea, E'\\022\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, E'\\111\\142\\147\\304\\132\\375\\070\\163\\270\\160\\251\\370\\126\\063\\351\\037\\257\\071\\143\\375\\351\\320\\253\\232\\220\\260\\075\\173\\306\\307\\115\\136'::bytea, 'key 2', E'\\254\\011\\315\\333\\273\\365\\001\\071\\024\\154\\253\\332\\301\\216\\361\\074\\221\\367\\251\\231\\274\\333\\300\\367\\001\\272\\327\\111\\315\\123\\042\\016'::bytea, NULL, '2019-02-14 08:28:24.267934+00');
INSERT INTO "value_attributions" ("project_id", "bucket_name", "partner_id", "last_updated") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, E''::bytea, E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea,'2019-02-14 08:07:31.028103+00');
INSERT INTO "user_credits" ("id", "user_id", "offer_id", "referred_by", "credits_earned_in_cents", "credits_used_in_cents", "type", "expires_at", "created_at") VALUES (1, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 1, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 200, 0, 'invalid', '2019-10-01 08:28:24.267934+00', '2019-06-01 08:28:24.267934+00');
INSERT INTO "bucket_metainfos" ("id", "project_id", "name", "partner_id", "created_at", "path_cipher", "default_segment_size", "default_encryption_cipher_suite", "default_encryption_block_size", "default_redundancy_algorithm", "default_redundancy_share_size", "default_redundancy_required_shares", "default_redundancy_repair_shares", "default_redundancy_optimal_shares", "default_redundancy_total_shares") VALUES (E'\\334/\\302;\\225\\355O\\323\\276f\\247\\354/6\\241\\033'::bytea, E'\\022\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, E'testbucketuniquename'::bytea, NULL, '2019-06-14 08:28:24.677953+00', 1, 65536, 1, 8192, 1, 4096, 4, 6, 8, 10);
INSERT INTO "pending_audits" ("node_id", "piece_id", "stripe_index", "share_size", "expected_share_hash", "reverify_count", "path") VALUES (E'\\153\\313\\233\\074\\327\\177\\136\\070\\346\\001'::bytea, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 5, 1024, E'\\070\\127\\144\\013\\332\\344\\102\\376\\306\\056\\303\\130\\106\\132\\321\\276\\321\\274\\170\\264\\054\\333\\221\\116\\154\\221\\335\\070\\220\\146\\344\\216'::bytea, 1, 'not null');
INSERT INTO "peer_identities" VALUES (E'\\334/\\302;\\225\\355O\\323\\276f\\247\\354/6\\241\\033'::bytea, E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2019-02-14 08:07:31.335028+00');
INSERT INTO "graceful_exit_progress" ("node_id", "bytes_transferred", "pieces_transferred", "pieces_failed", "updated_at") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', 1000000000000000, 0, 0, '2019-09-12 10:07:31.028103+00');
INSERT INTO "graceful_exit_transfer_queue" ("node_id", "path", "piece_num", "durability_ratio", "queued_at", "requested_at", "last_failed_at", "last_failed_code", "failed_count", "finished_at", "order_limit_send_count") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', E'f8419768-5baa-4901-b3ba-62808013ec45/s0/test3/\\240\\243\\223n \\334~b}\\2624)\\250m\\201\\202\\235\\276\\361\\3304\\323\\352\\311\\361\\353;\\326\\311', 8, 1.0, '2019-09-12 10:07:31.028103+00', '2019-09-12 10:07:32.028103+00', null, null, 0, '2019-09-12 10:07:33.028103+00', 0);
INSERT INTO "graceful_exit_transfer_queue" ("node_id", "path", "piece_num", "durability_ratio", "queued_at", "requested_at", "last_failed_at", "last_failed_code", "failed_count", "finished_at", "order_limit_send_count") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', E'f8419768-5baa-4901-b3ba-62808013ec45/s0/test3/\\240\\243\\223n \\334~b}\\2624)\\250m\\201\\202\\235\\276\\361\\3304\\323\\352\\311\\361\\353;\\326\\312', 8, 1.0, '2019-09-12 10:07:31.028103+00', '2019-09-12 10:07:32.028103+00', null, null, 0, '2019-09-12 10:07:33.028103+00', 0);
INSERT INTO "stripe_customers" ("user_id", "customer_id", "created_at") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 'stripe_id', '2019-06-01 08:28:24.267934+00');
INSERT INTO "graceful_exit_transfer_queue" ("node_id", "path", "piece_num", "durability_ratio", "queued_at", "requested_at", "last_failed_at", "last_failed_code", "failed_count", "finished_at", "order_limit_send_count") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', E'f8419768-5baa-4901-b3ba-62808013ec45/s0/test3/\\240\\243\\223n \\334~b}\\2624)\\250m\\201\\202\\235\\276\\361\\3304\\323\\352\\311\\361\\353;\\326\\311', 9, 1.0, '2019-09-12 10:07:31.028103+00', '2019-09-12 10:07:32.028103+00', null, null, 0, '2019-09-12 10:07:33.028103+00', 0);
INSERT INTO "graceful_exit_transfer_queue" ("node_id", "path", "piece_num", "durability_ratio", "queued_at", "requested_at", "last_failed_at", "last_failed_code", "failed_count", "finished_at", "order_limit_send_count") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', E'f8419768-5baa-4901-b3ba-62808013ec45/s0/test3/\\240\\243\\223n \\334~b}\\2624)\\250m\\201\\202\\235\\276\\361\\3304\\323\\352\\311\\361\\353;\\326\\312', 9, 1.0, '2019-09-12 10:07:31.028103+00', '2019-09-12 10:07:32.028103+00', null, null, 0, '2019-09-12 10:07:33.028103+00', 0);
INSERT INTO "stripecoinpayments_invoice_project_records"("id", "project_id", "storage", "egress", "objects", "period_start", "period_end", "state", "created_at") VALUES (E'\\022\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, E'\\021\\217/\\014\\376!K\\023\\276\\031\\311}m\\236\\205\\300'::bytea, 0, 0, 0, '2019-06-01 08:28:24.267934+00', '2019-06-01 08:28:24.267934+00', 0, '2019-06-01 08:28:24.267934+00');
INSERT INTO "graceful_exit_transfer_queue" ("node_id", "path", "piece_num", "root_piece_id", "durability_ratio", "queued_at", "requested_at", "last_failed_at", "last_failed_code", "failed_count", "finished_at", "order_limit_send_count") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\016', E'f8419768-5baa-4901-b3ba-62808013ec45/s0/test3/\\240\\243\\223n \\334~b}\\2624)\\250m\\201\\202\\235\\276\\361\\3304\\323\\352\\311\\361\\353;\\326\\311', 10, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 1.0, '2019-09-12 10:07:31.028103+00', '2019-09-12 10:07:32.028103+00', null, null, 0, '2019-09-12 10:07:33.028103+00', 0);
INSERT INTO "stripecoinpayments_tx_conversion_rates" ("tx_id", "rate", "created_at") VALUES ('tx_id', E'\\363\\311\\033w\\222\\303Ci,'::bytea, '2019-06-01 08:28:24.267934+00');
INSERT INTO "coinpayments_transactions" ("id", "user_id", "address", "amount", "received", "status", "key", "timeout", "created_at") VALUES ('tx_id', E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 'address', E'\\363\\311\\033w'::bytea, E'\\363\\311\\033w'::bytea, 1, 'key', 60, '2019-06-01 08:28:24.267934+00');
INSERT INTO "storagenode_bandwidth_rollups" ("storagenode_id", "interval_start", "interval_seconds", "action", "settled") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', '2020-01-11 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 2024);
INSERT INTO "coupons" ("id", "user_id", "amount", "description", "type", "status", "duration", "created_at") VALUES (E'\\362\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, 50, 'description', 0, 0, 2, '2019-06-01 08:28:24.267934+00');
INSERT INTO "coupon_usages" ("coupon_id", "amount", "status", "period") VALUES (E'\\362\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014'::bytea, 22, 0, '2019-06-01 09:28:24.267934+00');
INSERT INTO "stripecoinpayments_apply_balance_intents" ("tx_id", "state", "created_at") VALUES ('tx_id', 0, '2019-06-01 08:28:24.267934+00');
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "max_buckets", "rate_limit", "partner_id", "owner_id", "created_at") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\347'::bytea, 'projName1', 'Test project 1', NULL, NULL, NULL, 2000000, NULL, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2020-01-15 08:28:24.636949+00');
INSERT INTO "injuredsegments" ("path", "data", "segment_health", "updated_at") VALUES ('0', '\x0a0130120100', 1.0, '2020-09-01 00:00:00.000000+00');
INSERT INTO "injuredsegments" ("path", "data", "segment_health", "updated_at") VALUES ('here''s/a/great/path', '\x0a136865726527732f612f67726561742f70617468120a0102030405060708090a', 1.0, '2020-09-01 00:00:00.000000+00');
INSERT INTO "injuredsegments" ("path", "data", "segment_health", "updated_at") VALUES ('yet/another/cool/path', '\x0a157965742f616e6f746865722f636f6f6c2f70617468120a0102030405060708090a', 1.0, '2020-09-01 00:00:00.000000+00');
INSERT INTO "injuredsegments" ("path", "data", "segment_health", "updated_at") VALUES ('/this/is/a/new/path', '\x0a23736f2f6d616e792f69636f6e69632f70617468732f746f2f63686f6f73652f66726f6d120a0102030405060708090a', 1.0, '2020-09-01 00:00:00.000000+00');
INSERT INTO "injuredsegments" ("path", "data", "segment_health", "updated_at") VALUES ('/some/path/1/23/4', '\x0a23736f2f6d618e792f69636f6e69632f70617468732f746f2f63686f6f73652f66726f6d120a0102030405060708090a', 0.2, '2020-09-01 00:00:00.000000+00');
INSERT INTO "project_bandwidth_rollups"("project_id", "interval_month", egress_allocated) VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\347'::bytea, '2020-04-01', 10000);
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "max_buckets","rate_limit", "partner_id", "owner_id", "created_at") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\345'::bytea, 'egress101', 'High Bandwidth Project', NULL, NULL, NULL, 2000000, NULL, E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\204",'::bytea, '2020-05-15 08:46:24.000000+00');
INSERT INTO "storagenode_paystubs"("period", "node_id", "created_at", "codes", "usage_at_rest", "usage_get", "usage_put", "usage_get_repair", "usage_put_repair", "usage_get_audit", "comp_at_rest", "comp_get", "comp_put", "comp_get_repair", "comp_put_repair", "comp_get_audit", "surge_percent", "held", "owed", "disposed", "paid", "distributed") VALUES ('2020-01', '\xf2a3b4c4dfdf7221310382fd5db5aa73e1d227d6df09734ec4e5305000000000', '2020-04-07T20:14:21.479141Z', '', 1327959864508416, 294054066688, 159031363328, 226751, 0, 836608, 2861984, 5881081, 0, 226751, 0, 8, 300, 0, 26909472, 0, 26909472, 0);
INSERT INTO "nodes"("id", "address", "last_net", "protocol", "type", "email", "wallet", "free_disk", "piece_count", "major", "minor", "patch", "hash", "timestamp", "release","latency_90", "audit_success_count", "total_audit_count", "uptime_success_count", "total_uptime_count", "created_at", "updated_at", "last_contact_success", "last_contact_failure", "contained", "disqualified", "suspended", "audit_reputation_alpha", "audit_reputation_beta", "unknown_audit_reputation_alpha", "unknown_audit_reputation_beta", "exit_success", "unknown_audit_suspended", "offline_suspended", "under_review") VALUES (E'\\153\\313\\233\\074\\327\\255\\136\\070\\346\\001', '127.0.0.1:55516', '', 0, 4, '', '', -1, 0, 0, 1, 0, '', 'epoch', false, 0, 0, 5, 0, 5, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00', 'epoch', 'epoch', false, NULL, NULL, 50, 0, 1, 0, false, '2019-02-14 08:07:31.108963+00', '2019-02-14 08:07:31.108963+00', '2019-02-14 08:07:31.108963+00');
INSERT INTO "audit_histories" ("node_id", "history") VALUES (E'\\153\\313\\233\\074\\327\\177\\136\\070\\346\\001', '\x0a23736f2f6d616e792f69636f6e69632f70617468732f746f2f63686f6f73652f66726f6d120a0102030405060708090a');
INSERT INTO "node_api_versions"("id", "api_version", "created_at", "updated_at") VALUES (E'\\153\\313\\233\\074\\327\\177\\136\\070\\346\\001', 1, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00');
INSERT INTO "node_api_versions"("id", "api_version", "created_at", "updated_at") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', 2, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00');
INSERT INTO "node_api_versions"("id", "api_version", "created_at", "updated_at") VALUES (E'\\363\\342\\363\\371>+F\\256\\263\\300\\273|\\342N\\347\\014', 3, '2019-02-14 08:07:31.028103+00', '2019-02-14 08:07:31.108963+00');
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "rate_limit", "partner_id", "owner_id", "created_at", "max_buckets") VALUES (E'300\\273|\\342N\\347\\347\\363\\342\\363\\371>+F\\256\\263'::bytea, 'egress102', 'High Bandwidth Project 2', NULL, NULL, 2000000, NULL, E'265\\343U\\303\\312\\312\\363\\311\\033w\\222\\303Ci",'::bytea, '2020-05-15 08:46:24.000000+00', 1000);
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "rate_limit", "partner_id", "owner_id", "created_at", "max_buckets") VALUES (E'300\\273|\\342N\\347\\347\\363\\342\\363\\371>+F\\255\\244'::bytea, 'egress103', 'High Bandwidth Project 3', NULL, NULL, 2000000, NULL, E'265\\343U\\303\\312\\312\\363\\311\\033w\\222\\303Ci",'::bytea, '2020-05-15 08:46:24.000000+00', 1000);
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "rate_limit", "partner_id", "owner_id", "created_at", "max_buckets") VALUES (E'300\\273|\\342N\\347\\347\\363\\342\\363\\371>+F\\253\\231'::bytea, 'Limit Test 1', 'This project is above the default', 50000000001, 50000000001, 2000000, NULL, E'265\\343U\\303\\312\\312\\363\\311\\033w\\222\\303Ci",'::bytea, '2020-10-14 10:10:10.000000+00', 101);
INSERT INTO "projects"("id", "name", "description", "usage_limit", "bandwidth_limit", "rate_limit", "partner_id", "owner_id", "created_at", "max_buckets") VALUES (E'300\\273|\\342N\\347\\347\\363\\342\\363\\371>+F\\252\\230'::bytea, 'Limit Test 2', 'This project is below the default', NULL, NULL, 2000000, NULL, E'265\\343U\\303\\312\\312\\363\\311\\033w\\222\\303Ci",'::bytea, '2020-10-14 10:10:11.000000+00', NULL);
INSERT INTO "storagenode_bandwidth_rollups_phase2" ("storagenode_id", "interval_start", "interval_seconds", "action", "allocated", "settled") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', '2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024);
INSERT INTO "users"("id", "full_name", "short_name", "email", "normalized_email", "password_hash", "status", "partner_id", "created_at", "position", "company_name", "working_on", "company_size", "is_professional") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\205\\311",'::bytea, 'Thierry', 'Berg', '2email2@mail.test', '2EMAIL2@MAIL.TEST', E'some_readable_hash'::bytea, 2, NULL, '2020-05-16 10:28:24.614594+00', 'engineer', 'storj', 'data storage', 55, true);
INSERT INTO "storagenode_bandwidth_rollup_archives" ("storagenode_id", "interval_start", "interval_seconds", "action", "allocated", "settled") VALUES (E'\\006\\223\\250R\\221\\005\\365\\377v>0\\266\\365\\216\\255?\\347\\244\\371?2\\264\\262\\230\\007<\\001\\262\\263\\237\\247n', '2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024);
INSERT INTO "bucket_bandwidth_rollup_archives" ("bucket_name", "project_id", "interval_start", "interval_seconds", "action", "inline", "allocated", "settled") VALUES (E'testbucket'::bytea, E'\\170\\160\\157\\370\\274\\366\\113\\364\\272\\235\\301\\243\\321\\102\\321\\136'::bytea,'2019-03-06 08:00:00.000000' AT TIME ZONE current_setting('TIMEZONE'), 3600, 1, 1024, 2024, 3024);
INSERT INTO "storagenode_paystubs"("period", "node_id", "created_at", "codes", "usage_at_rest", "usage_get", "usage_put", "usage_get_repair", "usage_put_repair", "usage_get_audit", "comp_at_rest", "comp_get", "comp_put", "comp_get_repair", "comp_put_repair", "comp_get_audit", "surge_percent", "held", "owed", "disposed", "paid", "distributed") VALUES ('2020-12', '\x1111111111111111111111111111111111111111111111111111111111111111', '2020-04-07T20:14:21.479141Z', '', 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 117);
INSERT INTO "storagenode_payments"("id", "created_at", "period", "node_id", "amount") VALUES (1, '2020-04-07T20:14:21.479141Z', '2020-12', '\x1111111111111111111111111111111111111111111111111111111111111111', 117);
INSERT INTO "users"("id", "full_name", "short_name", "email", "normalized_email", "password_hash", "status", "partner_id", "created_at", "position", "company_name", "working_on", "company_size", "is_professional", "employee_count") VALUES (E'\\363\\311\\033w\\222\\303Ci\\265\\343U\\303\\312\\205\\312",'::bytea, 'Campbell', 'Wright', '4email4@mail.test', '4EMAIL4@MAIL.TEST', E'some_readable_hash'::bytea, 2, NULL, '2020-07-17 10:28:24.614594+00', 'engineer', 'storj', 'data storage', 82, true, '1-50');
-- NEW DATA --

View File

@ -84,6 +84,8 @@ uplink cp "sj://$BUCKET/diff-size-segments" "$DST_DIR" --progress=fal
uplink cp "sj://$BUCKET/put-file" "$DST_DIR" --progress=false
uplink cat "sj://$BUCKET/put-file" >> "$DST_DIR/put-file-from-cat"
uplink ls "sj://$BUCKET/small-upload-testfile" | grep "small-upload-testfile"
uplink rm "sj://$BUCKET/small-upload-testfile"
uplink rm "sj://$BUCKET/big-upload-testfile"
uplink rm "sj://$BUCKET/multisegment-upload-testfile"
@ -113,4 +115,4 @@ uplink rb "sj://$BUCKET" --force
if [ "$(uplink ls | grep "No buckets" | wc -l)" = "0" ]; then
echo "an integration test did not clean up after itself entirely"
exit 1
fi
fi

View File

@ -97,6 +97,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# id for google tag manager
# console.google-tag-manager-id: ""
# indicates if satellite is in beta
# console.is-beta-satellite: false
# url link to let us know page
# console.let-us-know-url: https://storjlabs.atlassian.net/servicedesk/customer/portals
@ -404,7 +407,7 @@ identity.key-path: /root/.local/share/storj/identity/satellite/identity.key
# orders.expiration: 48h0m0s
# how many items in the rollups write cache before they are flushed to the database
# orders.flush-batch-size: 10000
# orders.flush-batch-size: 1000
# how often to flush the rollups write cache to the database
# orders.flush-interval: 1m0s

View File

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
"storj.io/common/testcontext"
"storj.io/storj/storage/redis/redisserver"
"storj.io/storj/private/testredis"
"storj.io/storj/storage/testsuite"
)
@ -18,7 +18,7 @@ func TestSuite(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
redis, err := redisserver.Start(ctx)
redis, err := testredis.Start(ctx)
if err != nil {
t.Fatal(err)
}
@ -43,7 +43,7 @@ func TestInvalidConnection(t *testing.T) {
func BenchmarkSuite(b *testing.B) {
ctx := context.Background()
redis, err := redisserver.Start(ctx)
redis, err := testredis.Start(ctx)
if err != nil {
b.Fatal(err)
}

View File

@ -80,7 +80,7 @@ func (payout *Payout) PayStubMonthly(w http.ResponseWriter, r *http.Request) {
return
}
if err := json.NewEncoder(w).Encode([]*payouts.PayStub{payStub}); err != nil {
if err := json.NewEncoder(w).Encode(payStub); err != nil {
payout.log.Error("failed to encode json response", zap.Error(ErrPayoutAPI.Wrap(err)))
return
}

View File

@ -75,7 +75,7 @@ func TestHeldAmountApi(t *testing.T) {
paystub.UsageAtRest /= 720
expected, err := json.Marshal([]payouts.PayStub{paystub})
expected, err := json.Marshal(paystub)
require.NoError(t, err)
defer func() {
@ -92,7 +92,7 @@ func TestHeldAmountApi(t *testing.T) {
res2, err := http.Get(url)
require.NoError(t, err)
require.NotNil(t, res2)
require.Equal(t, http.StatusNotFound, res2.StatusCode)
require.Equal(t, http.StatusOK, res2.StatusCode)
defer func() {
err = res2.Body.Close()
@ -101,7 +101,7 @@ func TestHeldAmountApi(t *testing.T) {
body2, err := ioutil.ReadAll(res2.Body)
require.NoError(t, err)
expected = []byte("{\"error\":\"payouts console web error: payouts service error: no payStub for period error: sql: no rows in result set\"}\n")
expected = []byte("null\n")
require.Equal(t, expected, body2)
// should return 400 cause of wrong satellite id.

View File

@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"testing"
"time"
@ -17,6 +18,7 @@ import (
"storj.io/common/pb"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/storj/private/date"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/storagenode/payouts/estimatedpayouts"
@ -94,15 +96,19 @@ func TestStorageNodeApi(t *testing.T) {
})
require.NoError(t, err)
now2 := time.Now().UTC()
daysPerMonth := date.UTCEndOfMonth(now2).Day()
err = reputationdb.Store(ctx, reputation.Stats{
SatelliteID: satellite.ID(),
JoinedAt: time.Now().UTC(),
JoinedAt: now.AddDate(0, 0, -daysPerMonth+3),
})
require.NoError(t, err)
t.Run("test EstimatedPayout", func(t *testing.T) {
// should return estimated payout for both satellites in current month and empty for previous
url := fmt.Sprintf("%s/estimated-payout", baseURL)
res, err := http.Get(url)
require.NoError(t, err)
require.NotNil(t, res)
@ -115,16 +121,23 @@ func TestStorageNodeApi(t *testing.T) {
body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
bodyPayout := &estimatedpayouts.EstimatedPayout{}
require.NoError(t, json.Unmarshal(body, bodyPayout))
estimation, err := sno.Console.Service.GetAllSatellitesEstimatedPayout(ctx, time.Now())
require.NoError(t, err)
expected, err := json.Marshal(estimatedpayouts.EstimatedPayout{
expectedPayout := &estimatedpayouts.EstimatedPayout{
CurrentMonth: estimation.CurrentMonth,
PreviousMonth: estimation.PreviousMonth,
CurrentMonthExpectations: estimation.CurrentMonthExpectations,
})
}
require.NoError(t, err)
require.Equal(t, string(expected)+"\n", string(body))
// round CurrentMonthExpectations to 3 decimal places to resolve precision issues
bodyPayout.CurrentMonthExpectations = math.Floor(bodyPayout.CurrentMonthExpectations*1000) / 1000
expectedPayout.CurrentMonthExpectations = math.Floor(expectedPayout.CurrentMonthExpectations*1000) / 1000
require.EqualValues(t, expectedPayout, bodyPayout)
})
},
)

View File

@ -60,20 +60,49 @@ func (pm *PayoutMonthly) SetPayout() {
pm.Payout = RoundFloat(amount)
}
// Add sums payout monthly data.
func (pm *PayoutMonthly) Add(monthly PayoutMonthly) {
pm.Payout += monthly.Payout
pm.EgressRepairAuditPayout += monthly.EgressRepairAuditPayout
pm.DiskSpacePayout += monthly.DiskSpacePayout
pm.DiskSpace += monthly.DiskSpace
pm.EgressBandwidth += monthly.EgressBandwidth
pm.EgressBandwidthPayout += monthly.EgressBandwidthPayout
pm.EgressRepairAudit += monthly.EgressRepairAudit
pm.Held += monthly.Held
}
// RoundFloat rounds float value till 2 signs after dot.
func RoundFloat(value float64) float64 {
return math.Round(value*100) / 100
}
// SetExpectedMonth set current month expectations.
func (estimatedPayout *EstimatedPayout) SetExpectedMonth(now time.Time) {
daysPast := float64(now.Day()) - 1
if daysPast < 1 {
daysPast = 1
// Set set's estimated payout with current/previous PayoutMonthly's data and current month expectations.
func (estimatedPayout *EstimatedPayout) Set(current, previous PayoutMonthly, now, joinedAt time.Time) {
estimatedPayout.CurrentMonth = current
estimatedPayout.PreviousMonth = previous
daysSinceJoined := now.Sub(joinedAt).Hours() / 24
daysPerMonth := float64(date.UTCEndOfMonth(now).Day())
if daysSinceJoined >= float64(now.Day()) {
daysPast := float64(now.Day()) - 1
if daysPast < 1 {
daysPast = 1
}
payoutPerDay := estimatedPayout.CurrentMonth.Payout / daysPast
estimatedPayout.CurrentMonthExpectations += payoutPerDay * daysPerMonth
return
}
daysPerMonth := float64(date.UTCEndOfMonth(now).Day())
payoutPerDay := estimatedPayout.CurrentMonth.Payout / daysPast
estimatedPayout.CurrentMonthExpectations += payoutPerDay * daysPerMonth
estimatedPayout.CurrentMonthExpectations += estimatedPayout.CurrentMonth.Payout / daysSinceJoined * daysPerMonth
}
// Add adds estimate into the receiver.
func (estimatedPayout *EstimatedPayout) Add(other EstimatedPayout) {
estimatedPayout.CurrentMonth.Add(other.CurrentMonth)
estimatedPayout.PreviousMonth.Add(other.PreviousMonth)
estimatedPayout.CurrentMonthExpectations += other.CurrentMonthExpectations
}

View File

@ -10,41 +10,309 @@ import (
"github.com/stretchr/testify/require"
"storj.io/common/testcontext"
"storj.io/storj/private/testplanet"
"storj.io/storj/storagenode/payouts/estimatedpayouts"
)
func TestCurrentMonthExpectations(t *testing.T) {
testplanet.Run(t, testplanet.Config{
StorageNodeCount: 1,
SatelliteCount: 2,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
const payout = 100.0
const payout = 100.0
type test struct {
time time.Time
expected float64
}
tests := []test{
// 28 days in month
{time.Date(2021, 2, 1, 16, 0, 0, 0, time.UTC), 2800.00},
{time.Date(2021, 2, 28, 10, 0, 0, 0, time.UTC), 103.70},
// 31 days in month
{time.Date(2021, 3, 1, 19, 0, 0, 0, time.UTC), 3100.0},
{time.Date(2021, 3, 31, 21, 0, 0, 0, time.UTC), 103.33},
}
type test struct {
time time.Time
expected float64
joinedAt time.Time
payout estimatedpayouts.EstimatedPayout
current, previous estimatedpayouts.PayoutMonthly
}
tests := []test{
// 28 days in month
{time.Date(2021, 2, 1, 16, 0, 0, 0, time.UTC), 2800.00, time.Date(2021, 1, 1, 12, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
{time.Date(2021, 2, 28, 10, 0, 0, 0, time.UTC), 103.70, time.Date(2021, 1, 26, 10, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
{time.Date(2021, 2, 28, 10, 0, 0, 0, time.UTC), 215.38, time.Date(2021, 2, 15, 10, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
// 31 days in month
{time.Date(2021, 3, 1, 19, 0, 0, 0, time.UTC), 3100.0, time.Date(2021, 1, 1, 19, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
{time.Date(2021, 3, 31, 21, 0, 0, 0, time.UTC), 103.33, time.Date(2021, 1, 31, 21, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
{time.Date(2021, 3, 31, 21, 0, 0, 0, time.UTC), 193.75, time.Date(2021, 3, 15, 21, 0, 0, 0, time.UTC),
estimatedpayouts.EstimatedPayout{},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
},
estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 567,
DiskSpacePayout: 678,
HeldRate: 789,
Payout: payout,
Held: 901,
}},
}
for _, test := range tests {
estimates := estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
Payout: payout,
},
}
estimates.SetExpectedMonth(test.time)
require.False(t, math.IsNaN(estimates.CurrentMonthExpectations))
require.InDelta(t, test.expected, estimates.CurrentMonthExpectations, 0.01)
}
})
for _, test := range tests {
test.payout.Set(test.current, test.previous, test.time, test.joinedAt)
require.False(t, math.IsNaN(test.payout.CurrentMonthExpectations))
require.InDelta(t, test.expected, test.payout.CurrentMonthExpectations, 0.01)
require.Equal(t, test.payout.CurrentMonth, test.current)
require.Equal(t, test.payout.PreviousMonth, test.previous)
}
}
func TestAddEstimationPayout(t *testing.T) {
type test struct {
basic, addition, result estimatedpayouts.EstimatedPayout
}
tests := []test{
{estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 123,
EgressRepairAudit: 123,
EgressRepairAuditPayout: 123,
DiskSpace: 123,
DiskSpacePayout: 123,
Payout: 123,
Held: 123,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 234,
EgressBandwidthPayout: 234,
EgressRepairAudit: 234,
EgressRepairAuditPayout: 234,
DiskSpace: 234,
DiskSpacePayout: 234,
Payout: 234,
Held: 234,
},
CurrentMonthExpectations: 111,
},
estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 345,
EgressBandwidthPayout: 345,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 345,
DiskSpace: 345,
DiskSpacePayout: 345,
Payout: 345,
Held: 345,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 456,
EgressBandwidthPayout: 456,
EgressRepairAudit: 456,
EgressRepairAuditPayout: 456,
DiskSpace: 456,
DiskSpacePayout: 456,
Payout: 456,
Held: 456,
},
CurrentMonthExpectations: 222,
},
estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 468,
EgressBandwidthPayout: 468,
EgressRepairAudit: 468,
EgressRepairAuditPayout: 468,
DiskSpace: 468,
DiskSpacePayout: 468,
Payout: 468,
Held: 468,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 690,
EgressBandwidthPayout: 690,
EgressRepairAudit: 690,
EgressRepairAuditPayout: 690,
DiskSpace: 690,
DiskSpacePayout: 690,
Payout: 690,
Held: 690,
},
CurrentMonthExpectations: 333,
}},
{estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
DiskSpace: 123,
DiskSpacePayout: 234,
Payout: 345,
Held: 456,
},
CurrentMonthExpectations: 111,
},
estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
DiskSpace: 456,
DiskSpacePayout: 345,
Payout: 234,
Held: 123,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 456,
EgressBandwidthPayout: 345,
EgressRepairAudit: 234,
EgressRepairAuditPayout: 123,
},
CurrentMonthExpectations: 111,
},
estimatedpayouts.EstimatedPayout{
CurrentMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 123,
EgressBandwidthPayout: 234,
EgressRepairAudit: 345,
EgressRepairAuditPayout: 456,
DiskSpace: 456,
DiskSpacePayout: 345,
Payout: 234,
Held: 123,
},
PreviousMonth: estimatedpayouts.PayoutMonthly{
EgressBandwidth: 456,
EgressBandwidthPayout: 345,
EgressRepairAudit: 234,
EgressRepairAuditPayout: 123,
DiskSpace: 123,
DiskSpacePayout: 234,
Payout: 345,
Held: 456,
},
CurrentMonthExpectations: 222,
}},
}
for _, test := range tests {
test.basic.Add(test.addition)
require.Equal(t, test.basic, test.result)
}
}

View File

@ -61,22 +61,12 @@ func (s *Service) GetSatelliteEstimatedPayout(ctx context.Context, satelliteID s
return EstimatedPayout{}, EstimationServiceErr.Wrap(err)
}
payout.CurrentMonth = currentMonthPayout
payout.PreviousMonth = previousMonthPayout
stats, err := s.reputationDB.Get(ctx, satelliteID)
if err != nil {
return EstimatedPayout{}, EstimationServiceErr.Wrap(err)
}
daysSinceJoined := now.Sub(stats.JoinedAt).Hours() / 24
if daysSinceJoined >= float64(now.Day()) {
payout.SetExpectedMonth(now)
return payout, nil
}
payout.CurrentMonthExpectations = (payout.CurrentMonth.Payout / daysSinceJoined) * float64(date.UTCEndOfMonth(now).Day())
payout.Set(currentMonthPayout, previousMonthPayout, now, stats.JoinedAt)
return payout, nil
}
@ -91,25 +81,16 @@ func (s *Service) GetAllSatellitesEstimatedPayout(ctx context.Context, now time.
return EstimatedPayout{}, EstimationServiceErr.Wrap(err)
}
payout.CurrentMonth.Payout += current.Payout
payout.CurrentMonth.EgressRepairAuditPayout += current.EgressRepairAuditPayout
payout.CurrentMonth.DiskSpacePayout += current.DiskSpacePayout
payout.CurrentMonth.DiskSpace += current.DiskSpace
payout.CurrentMonth.EgressBandwidth += current.EgressBandwidth
payout.CurrentMonth.EgressBandwidthPayout += current.EgressBandwidthPayout
payout.CurrentMonth.EgressRepairAudit += current.EgressRepairAudit
payout.CurrentMonth.Held += current.Held
payout.PreviousMonth.Payout += previous.Payout
payout.PreviousMonth.DiskSpacePayout += previous.DiskSpacePayout
payout.PreviousMonth.DiskSpace += previous.DiskSpace
payout.PreviousMonth.EgressBandwidth += previous.EgressBandwidth
payout.PreviousMonth.EgressBandwidthPayout += previous.EgressBandwidthPayout
payout.PreviousMonth.EgressRepairAuditPayout += previous.EgressRepairAuditPayout
payout.PreviousMonth.EgressRepairAudit += previous.EgressRepairAudit
payout.PreviousMonth.Held += previous.Held
}
var satellitePayout EstimatedPayout
payout.SetExpectedMonth(now)
stats, err := s.reputationDB.Get(ctx, satelliteIDs[i])
if err != nil {
return EstimatedPayout{}, EstimationServiceErr.Wrap(err)
}
satellitePayout.Set(current, previous, now, stats.JoinedAt)
payout.Add(satellitePayout)
}
return payout, nil
}

View File

@ -115,6 +115,7 @@ type SatellitePayoutForPeriod struct {
Paid int64 `json:"paid"`
Receipt string `json:"receipt"`
IsExitComplete bool `json:"isExitComplete"`
Distributed int64 `json:"distributed"`
}
// Period is a string that represents paystub period type in format yyyy-mm.

View File

@ -70,6 +70,10 @@ func (service *Service) SatellitePayStubMonthly(ctx context.Context, satelliteID
payStub, err = service.db.GetPayStub(ctx, satelliteID, period)
if err != nil {
if ErrNoPayStubForPeriod.Has(err) {
return nil, nil
}
return nil, ErrPayoutService.Wrap(err)
}
@ -304,6 +308,7 @@ func (service *Service) AllSatellitesPayoutPeriod(ctx context.Context, period st
payoutForPeriod.SurgePercent = paystub.SurgePercent
payoutForPeriod.Paid = paystub.Paid
payoutForPeriod.HeldPercent = heldPercent
payoutForPeriod.Distributed = paystub.Distributed
result = append(result, payoutForPeriod)
}

View File

@ -15,6 +15,7 @@
<meta name="general-request-url" content="{{ .GeneralRequestURL }}">
<meta name="project-limits-increase-request-url" content="{{ .ProjectLimitsIncreaseRequestURL }}">
<meta name="gateway-credentials-request-url" content="{{ .GatewayCredentialsRequestURL }}">
<meta name="is-beta-satellite" content="{{ .IsBetaSatellite }}">
<title>{{ .SatelliteName }}</title>
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAL8SURBVHgBxVcxTFNRFL01JvylrRsmNWETWWSybg5I3NTQBCdRFyfrDuoKMgMyOQgdTcDAJmrUrWWCgQIbhJKy0TJQpsc73P/S9/tfP+8/GnqSn1fo77/3nnPufe8nhAR1ETeoy7hJjpj93/ycu0+UuUVuEA5Y2xaifirEVpX/nvknnBFbgpMGUfmIKOkRLRT578oxXy6IJcFCialH0EyaaPoZBy7tEQ3NEY1IKd4/iidHwqYLijLA559cuY6dT0RjBU5AAYm9fiivLFnBKMGBTyeqQ4BXhXDwdqjUiKZkskOzREsbzeeBNRMCEiDgr12uYl1WNbnW/oc2iUys8jrQyyxhHRkM3hdgAMFBHQyGG/GDqyDlsSeS/npQC99jlEBpOnyX2XCF8sGhZLbeMLMZkCDbJ1nYYTfDeMP9fMH5y5vmIKYE8RxUjBXPedDH1Zu6I9QFSzLQxErz4Xn5oNwg+2NSmuv3Lkvz4QlTi8rupDlBmA6tqQLrnYNCvoxSNAOtUEaakwzMv+ALidTP2OlKKiSK75Cs6hy9NYFkjzmG1SBCIuUq0Za8pgydge8R9E+e10qNrGE1ikH5435mo11bQgr4B9LEgVUC0Npm1o+vcuvBxB1NYFsaaeC2XUuW/Xs7msC9Xqa+MMa9jQr1KtXAQoKYHakeskbIhDrVasdTbbVY4s8ZYld/9PWuyeTSHksFBjBFcZ+aH/j/yZk5gcAcgImgIX6MNsKKhKBta1sB2A3HV5pD6iJQIzw/MICwoohc1F6ALBH03XemFYPl+VdzcBNUh6j5gZZEcP341opAAnX/AXl/A0FlrrshgMRR+YUvPPN8CHgAxlqWVYuEdH7V/ZilA6cosFDa53EcmUDKC+7X+IwxHEVhO0DK6aeXH88uHcWQA8xE7Yg69M6xgdWZUEFtNNDyx1s2KnyDIxu22zdZTjgWhANm/vL6clGIsnw3+Fbk94RreS8AMGrBxvwoT0lMPnSNC2JJoAPdgnMBJLjKq5lzAp1C19+OzwFiYzAU5f7eeQAAAABJRU5ErkJggg==" type="image/x-icon">
<link rel="dns-prefetch" href="https://js.stripe.com">

View File

@ -2159,6 +2159,115 @@
"yorkie": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
"integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"dev": true,
"optional": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"tslint": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz",
@ -2179,6 +2288,13 @@
"tslib": "^1.8.0",
"tsutils": "^2.29.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"optional": true
}
}
},
@ -2358,6 +2474,17 @@
"unique-filename": "^1.1.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -2453,6 +2580,18 @@
"graceful-fs": "^4.1.6"
}
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
@ -2560,6 +2699,18 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -4406,9 +4557,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001131",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001131.tgz",
"integrity": "sha512-4QYi6Mal4MMfQMSqGIRPGbKIbZygeN83QsWq1ixpUwvtfgAZot5BrCKzGygvZaV+CnELdTwD0S4cqUNozq7/Cw==",
"version": "1.0.30001191",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz",
"integrity": "sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==",
"dev": true
},
"capture-exit": {
@ -7408,122 +7559,6 @@
"worker-rpc": "^0.1.0"
}
},
"fork-ts-checker-webpack-plugin-v5": {
"version": "npm:fork-ts-checker-webpack-plugin@5.2.1",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.1.tgz",
"integrity": "sha512-SVi+ZAQOGbtAsUWrZvGzz38ga2YqjWvca1pXQFUArIVXqli0lLoDQ8uS0wg0kSpcwpZmaW5jVCZXQebkyUQSsw==",
"dev": true,
"optional": true,
"requires": {
"@babel/code-frame": "^7.8.3",
"@types/json-schema": "^7.0.5",
"chalk": "^4.1.0",
"cosmiconfig": "^6.0.0",
"deepmerge": "^4.2.2",
"fs-extra": "^9.0.0",
"memfs": "^3.1.2",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"optional": true,
"requires": {
"yallist": "^4.0.0"
}
},
"schema-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
"dev": true,
"optional": true,
"requires": {
"@types/json-schema": "^7.0.4",
"ajv": "^6.12.2",
"ajv-keywords": "^3.4.1"
}
},
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"dev": true,
"optional": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true,
"optional": true
}
}
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
@ -16755,87 +16790,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.2.tgz",
"integrity": "sha512-8QTxh+Fd+HB6fiL52iEVLKqE9N1JSlMXLR92Ijm6g8PZrwIxckgpqjPDWRP5TWxdiPaHR+alUWsnu1ShQOwt+Q==",
"dev": true,
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"loader-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
"dev": true,
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-parser": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/vue-parser/-/vue-parser-1.1.6.tgz",

View File

@ -29,12 +29,17 @@ export default class App extends Vue {
*/
public mounted(): void {
const satelliteName = MetaUtils.getMetaContent('satellite-name');
const isBetaSatellite = MetaUtils.getMetaContent('is-beta-satellite');
const segmentioId = MetaUtils.getMetaContent('segment-io');
if (satelliteName) {
this.$store.dispatch(APP_STATE_ACTIONS.SET_SATELLITE_NAME, satelliteName);
}
if (isBetaSatellite) {
this.$store.dispatch(APP_STATE_ACTIONS.SET_SATELLITE_STATUS, isBetaSatellite === 'true');
}
if (segmentioId) {
this.$segment.init(segmentioId);
}

View File

@ -1,150 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql';
import { ApiKey, ApiKeyCursor, ApiKeysApi, ApiKeysPage } from '@/types/apiKeys';
/**
* ApiKeysApiGql is a graphql implementation of ApiKeys API.
* Exposes all apiKey-related functionality
*/
export class ApiKeysApiGql extends BaseGql implements ApiKeysApi {
/**
* Fetch apiKeys.
*
* @returns ApiKey
* @throws Error
*/
public async get(projectId: string, cursor: ApiKeyCursor): Promise<ApiKeysPage> {
const query =
`query($projectId: String!, $limit: Int!, $search: String!, $page: Int!, $order: Int!, $orderDirection: Int!) {
project (
id: $projectId,
) {
apiKeys (
cursor: {
limit: $limit,
search: $search,
page: $page,
order: $order,
orderDirection: $orderDirection
}
) {
apiKeys {
id,
name,
createdAt
}
search,
limit,
order,
pageCount,
currentPage,
totalCount
}
}
}`;
const variables = {
projectId: projectId,
limit: cursor.limit,
search: cursor.search,
page: cursor.page,
order: cursor.order,
orderDirection: cursor.orderDirection,
};
const response = await this.query(query, variables);
return this.getApiKeysPage(response.data.project.apiKeys);
}
/**
* Used to create apiKey.
*
* @param projectId - stores current project id
* @param name - name of apiKey that will be created
* @returns ApiKey
* @throws Error
*/
public async create(projectId: string, name: string): Promise<ApiKey> {
const query =
`mutation($projectId: String!, $name: String!) {
createAPIKey(
projectID: $projectId,
name: $name
) {
key,
keyInfo {
id,
name,
createdAt
}
}
}`;
const variables = {
projectId,
name,
};
const response = await this.mutate(query, variables);
const key: any = response.data.createAPIKey.keyInfo;
const secret: string = response.data.createAPIKey.key;
return new ApiKey(key.id, key.name, key.createdAt, secret);
}
/**
* Used to delete apiKey.
*
* @param ids - ids of apiKeys that will be deleted
* @throws Error
*/
public async delete(ids: string[]): Promise<void> {
const query =
`mutation($id: [String!]!) {
deleteAPIKeys(id: $id) {
id
}
}`;
const variables = {
id: ids,
};
const response = await this.mutate(query, variables);
return response.data.deleteAPIKeys;
}
/**
* Method for mapping api keys page from json to ApiKeysPage type.
*
* @param page anonymous object from json
*/
private getApiKeysPage(page: any): ApiKeysPage {
if (!page) {
return new ApiKeysPage();
}
const apiKeysPage: ApiKeysPage = new ApiKeysPage();
apiKeysPage.apiKeys = page.apiKeys.map(key => new ApiKey(
key.id,
key.name,
new Date(key.createdAt),
'',
));
apiKeysPage.search = page.search;
apiKeysPage.limit = page.limit;
apiKeysPage.order = page.order;
apiKeysPage.orderDirection = page.orderDirection;
apiKeysPage.pageCount = page.pageCount;
apiKeysPage.currentPage = page.currentPage;
apiKeysPage.totalCount = page.totalCount;
return apiKeysPage;
}
}

View File

@ -205,7 +205,7 @@ export class AuthHttpApi {
* @returns id of created user
* @throws Error
*/
public async register(user: { fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string }, secret: string): Promise<string> {
public async register(user: {fullName: string; shortName: string; email: string; partner: string; partnerId: string; password: string; isProfessional: boolean; position: string; companyName: string; employeeCount: string}, secret: string): Promise<string> {
const path = `${this.ROOT_PATH}/register`;
const body = {
secret: secret,
@ -215,6 +215,10 @@ export class AuthHttpApi {
email: user.email,
partner: user.partner ? user.partner : '',
partnerId: user.partnerId ? user.partnerId : '',
isProfessional: user.isProfessional,
position: user.position,
companyName: user.companyName,
employeeCount: user.employeeCount,
};
const response = await this.http.post(path, JSON.stringify(body));

View File

@ -42,7 +42,13 @@ import VButton from '@/components/common/VButton.vue';
import { RouteConfig } from '@/router';
import { ACCESS_GRANTS_ACTIONS } from '@/store/modules/accessGrants';
import { BUCKET_ACTIONS } from '@/store/modules/buckets';
import { PAYMENTS_ACTIONS } from '@/store/modules/payments';
import { PROJECTS_ACTIONS } from '@/store/modules/projects';
import { AccessGrant } from '@/types/accessGrants';
import { ProjectFields } from '@/types/projects';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
@Component({
components: {
@ -92,6 +98,41 @@ export default class NameStep extends Vue {
this.isLoading = true;
// Check if at least one project exists.
// Used like backwards compatibility for the old accounts without any project.
if (this.$store.getters.projects.length === 0) {
try {
const FIRST_PAGE = 1;
const UNTITLED_PROJECT_NAME = 'Untitled Project';
const UNTITLED_PROJECT_DESCRIPTION = '___';
const project = new ProjectFields(
UNTITLED_PROJECT_NAME,
UNTITLED_PROJECT_DESCRIPTION,
this.$store.getters.user.id,
);
const createdProject = await this.$store.dispatch(PROJECTS_ACTIONS.CREATE, project);
this.$segment.track(SegmentEvent.PROJECT_CREATED, {
project_id: createdProject.id,
});
await this.$store.dispatch(PROJECTS_ACTIONS.SELECT, createdProject.id);
await this.$store.dispatch(PM_ACTIONS.CLEAR);
await this.$store.dispatch(PM_ACTIONS.FETCH, FIRST_PAGE);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PAYMENTS_HISTORY);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_BALANCE);
await this.$store.dispatch(PAYMENTS_ACTIONS.GET_PROJECT_USAGE_AND_CHARGES_CURRENT_ROLLUP);
await this.$store.dispatch(PROJECTS_ACTIONS.GET_LIMITS, createdProject.id);
await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CLEAR);
await this.$store.dispatch(BUCKET_ACTIONS.CLEAR);
} catch (error) {
await this.$notify.error(error.message);
this.isLoading = false;
return;
}
}
let createdAccessGrant: AccessGrant;
try {
createdAccessGrant = await this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CREATE, this.name);

View File

@ -1,545 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="api-keys-area">
<h1 class="api-keys-area__title" v-if="!isEmpty">API Keys</h1>
<div class="api-keys-area__container">
<ApiKeysCreationPopup
@closePopup="closeNewApiKeyPopup"
@showCopyPopup="showCopyApiKeyPopup"
:is-popup-shown="isNewApiKeyPopupShown"
/>
<ApiKeysCopyPopup
:is-popup-shown="isCopyApiKeyPopupShown"
:api-key-secret="apiKeySecret"
@closePopup="closeCopyNewApiKeyPopup"
/>
<div v-if="isHeaderShown" class="api-keys-header">
<VHeader
ref="headerComponent"
placeholder="API Key"
:search="onSearchQueryCallback">
<div class="header-default-state" v-if="isDefaultHeaderState">
<VButton
class="button"
label="+ Create API Key"
width="180px"
height="48px"
:on-press="onCreateApiKeyClick"
/>
</div>
<div class="header-selected-api-keys" v-if="areApiKeysSelected">
<span class="header-selected-api-keys__confirmation-label" v-if="isDeleteClicked">
Are you sure you want to delete <b>{{selectedAPIKeysCount}}</b> {{apiKeyCountTitle}} ?
</span>
<div class="header-selected-api-keys__buttons-area">
<VButton
class="button deletion"
label="Delete"
width="122px"
height="48px"
:on-press="onDeleteClick"
/>
<VButton
class="button"
label="Cancel"
width="122px"
height="48px"
is-transparent="true"
:on-press="onClearSelection"
/>
<span class="header-selected-api-keys__info-text" v-if="!isDeleteClicked">
<b>{{selectedAPIKeysCount}}</b> API Keys selected
</span>
</div>
</div>
</VHeader>
<div class="blur-content" v-if="isDeleteClicked"></div>
<div class="blur-search" v-if="isDeleteClicked"></div>
</div>
<div v-if="!isEmpty" class="api-keys-items">
<SortingHeader :on-header-click-callback="onHeaderSectionClickCallback"/>
<div class="api-keys-items__content">
<VList
:data-set="apiKeyList"
:item-component="itemComponent"
:on-item-click="toggleSelection"
/>
</div>
<VPagination
v-if="totalPageCount > 1"
class="pagination-area"
ref="pagination"
:total-page-count="totalPageCount"
:on-page-click-callback="onPageClick"
/>
</div>
<div class="empty-search-result-area" v-if="isEmptySearchResultShown">
<h1 class="empty-search-result-area__title">No results found</h1>
<EmptySearchResultIcon class="empty-search-result-area__image"/>
</div>
<NoApiKeysArea
:on-button-click="onCreateApiKeyClick"
v-if="isEmptyStateShown"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import ApiKeysItem from '@/components/apiKeys/ApiKeysItem.vue';
import NoApiKeysArea from '@/components/apiKeys/NoApiKeysArea.vue';
import SortingHeader from '@/components/apiKeys/SortingHeader.vue';
import VButton from '@/components/common/VButton.vue';
import VHeader from '@/components/common/VHeader.vue';
import VList from '@/components/common/VList.vue';
import VPagination from '@/components/common/VPagination.vue';
import EmptySearchResultIcon from '@/../static/images/common/emptySearchResult.svg';
import { API_KEYS_ACTIONS } from '@/store/modules/apiKeys';
import { ApiKey, ApiKeyOrderBy } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
import ApiKeysCopyPopup from './ApiKeysCopyPopup.vue';
import ApiKeysCreationPopup from './ApiKeysCreationPopup.vue';
// header state depends on api key selection state
/**
* HeaderState is enumerable of page's header states.
* Depends on api key selection state.
*/
enum HeaderState {
DEFAULT = 0,
ON_SELECT,
}
const {
FETCH,
DELETE,
TOGGLE_SELECTION,
CLEAR,
CLEAR_SELECTION,
SET_SEARCH_QUERY,
SET_SORT_BY,
SET_SORT_DIRECTION,
} = API_KEYS_ACTIONS;
declare interface ResetPagination {
resetPageIndex(): void;
}
@Component({
components: {
NoApiKeysArea,
VList,
VHeader,
ApiKeysItem,
VButton,
ApiKeysCreationPopup,
ApiKeysCopyPopup,
VPagination,
SortingHeader,
EmptySearchResultIcon,
},
})
export default class ApiKeysArea extends Vue {
private FIRST_PAGE = 1;
/**
* Indicates if delete confirmation state should appear.
*/
private isDeleteClicked: boolean = false;
/**
* Indicates if api key name input state should appear.
*/
private isNewApiKeyPopupShown: boolean = false;
/**
* Indicates if copy api key state should appear.
* Should only appear once
*/
private isCopyApiKeyPopupShown: boolean = false;
private apiKeySecret: string = '';
public $refs!: {
pagination: HTMLElement & ResetPagination;
};
/**
* Lifecycle hook after initial render where list of existing api keys is fetched.
*/
public async mounted(): Promise<void> {
await this.$store.dispatch(FETCH, 1);
this.$segment.track(SegmentEvent.API_KEYS_VIEWED, {
project_id: this.$store.getters.selectedProject.id,
api_keys_count: this.selectedAPIKeysCount,
});
}
/**
* Lifecycle hook before component destruction.
* Clears existing api keys selection and search.
*/
public async beforeDestroy(): Promise<void> {
this.onClearSelection();
await this.$store.dispatch(SET_SEARCH_QUERY, '');
}
/**
* Toggles api key selection.
* @param apiKey
*/
public async toggleSelection(apiKey: ApiKey): Promise<void> {
await this.$store.dispatch(TOGGLE_SELECTION, apiKey);
}
/**
* Starts creating API key process.
* Makes Create API Key popup visible.
*/
public onCreateApiKeyClick(): void {
this.isNewApiKeyPopupShown = true;
}
/**
* Holds on button click login for deleting API key process.
*/
public onDeleteClick(): void {
if (!this.isDeleteClicked) {
this.isDeleteClicked = true;
return;
}
this.delete();
}
/**
* Clears API Keys selection.
*/
public onClearSelection(): void {
this.$store.dispatch(CLEAR_SELECTION);
this.isDeleteClicked = false;
}
/**
* Closes Create API Key popup.
*/
public closeNewApiKeyPopup(): void {
this.isNewApiKeyPopupShown = false;
}
/**
* Closes Create API Key popup.
*/
public showCopyApiKeyPopup(secret: string): void {
this.isCopyApiKeyPopupShown = true;
this.apiKeySecret = secret;
}
/**
* Makes Copy API Key popup visible
*/
public closeCopyNewApiKeyPopup(): void {
this.isCopyApiKeyPopupShown = false;
}
/**
* Deletes selected api keys, fetches updated list and changes area state to default.
*/
private async delete(): Promise<void> {
try {
await this.$store.dispatch(DELETE);
await this.$notify.success(`API keys deleted successfully`);
this.$segment.track(SegmentEvent.API_KEY_DELETED, {
project_id: this.$store.getters.selectedProject.id,
});
} catch (error) {
await this.$notify.error(error.message);
}
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
this.isDeleteClicked = false;
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
/**
* Returns API Key item component.
*/
public get itemComponent() {
return ApiKeysItem;
}
/**
* Returns api keys from store.
*/
public get apiKeyList(): ApiKey[] {
return this.$store.getters.apiKeys;
}
/**
* Returns api keys pages count from store.
*/
public get totalPageCount(): number {
return this.$store.state.apiKeysModule.page.pageCount;
}
/**
* Returns api keys label depends on api keys count.
*/
public get apiKeyCountTitle(): string {
return this.selectedAPIKeysCount === 1 ? 'api key' : 'api keys';
}
/**
* Indicates if no api keys in store.
*/
public get isEmpty(): boolean {
return this.$store.getters.apiKeys.length === 0;
}
/**
* Indicates if there is search query in store.
*/
public get hasSearchQuery(): boolean {
return this.$store.state.apiKeysModule.cursor.search;
}
/**
* Returns amount of selected API Keys from store.
*/
public get selectedAPIKeysCount(): number {
return this.$store.state.apiKeysModule.selectedApiKeysIds.length;
}
/**
* Returns page's header state depending on selected API Keys amount.
*/
public get headerState(): number {
return this.selectedAPIKeysCount > 0 ? HeaderState.ON_SELECT : HeaderState.DEFAULT;
}
/**
* Indicates if page's header is shown.
*/
public get isHeaderShown(): boolean {
return !this.isEmpty || this.hasSearchQuery;
}
/**
* Indicates if page's header is in default state.
*/
public get isDefaultHeaderState(): boolean {
return this.headerState === HeaderState.DEFAULT;
}
/**
* Indicates if page's header is in selected state.
*/
public get areApiKeysSelected(): boolean {
return this.headerState === HeaderState.ON_SELECT;
}
/**
* Indicates if page is in empty search result state.
*/
public get isEmptySearchResultShown(): boolean {
return this.isEmpty && this.hasSearchQuery;
}
/**
* Indicates if page is in empty state.
*/
public get isEmptyStateShown(): boolean {
return this.isEmpty && !this.isNewApiKeyPopupShown && !this.hasSearchQuery;
}
/**
* Fetches api keys page by clicked index.
* @param index
*/
public async onPageClick(index: number): Promise<void> {
try {
await this.$store.dispatch(FETCH, index);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
}
/**
* Used for sorting.
* @param sortBy
* @param sortDirection
*/
public async onHeaderSectionClickCallback(sortBy: ApiKeyOrderBy, sortDirection: SortDirection): Promise<void> {
await this.$store.dispatch(SET_SORT_BY, sortBy);
await this.$store.dispatch(SET_SORT_DIRECTION, sortDirection);
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
/**
* Sets api keys search query and then fetches depends on it.
* @param query
*/
public async onSearchQueryCallback(query: string): Promise<void> {
await this.$store.dispatch(SET_SEARCH_QUERY, query);
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex();
}
}
}
</script>
<style scoped lang="scss">
.api-keys-area {
position: relative;
padding: 40px 30px 55px 30px;
font-family: 'font_regular', sans-serif;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 39px;
color: #263549;
margin: 0;
}
.api-keys-header {
width: 100%;
position: relative;
.blur-content {
position: absolute;
top: 100%;
left: 0;
background-color: #f5f6fa;
width: 100%;
height: 70vh;
z-index: 100;
opacity: 0.3;
}
.blur-search {
position: absolute;
bottom: 0;
right: 0;
width: 540px;
height: 56px;
z-index: 100;
opacity: 0.3;
background-color: #f5f6fa;
}
}
.api-keys-items {
position: relative;
&__content {
display: flex;
flex-direction: column;
width: 100%;
justify-content: flex-start;
}
}
}
.empty-search-result-area {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 32px;
line-height: 39px;
margin-top: 104px;
}
&__image {
margin-top: 40px;
}
}
.pagination-area {
margin-left: -25px;
padding-bottom: 15px;
}
.header-default-state {
display: flex;
align-items: center;
}
.header-selected-api-keys {
&__confirmation-label {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 28px;
}
&__buttons-area {
display: flex;
align-items: center;
.deletion {
margin-right: 12px;
}
}
&__info-text {
margin-left: 25px;
line-height: 48px;
}
}
.container.deletion {
background-color: #ff4f4d;
&.label {
color: #fff;
}
&:hover {
background-color: #de3e3d;
box-shadow: none;
}
}
.collapsed {
margin-top: 0 !important;
padding-top: 0 !important;
}
::-webkit-scrollbar,
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {
width: 0;
}
</style>

View File

@ -1,183 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="save-api-popup" v-if="isPopupShown">
<h2 class="save-api-popup__title">Save Your Secret API Key! It Will Appear Only Once.</h2>
<div class="save-api-popup__copy-area">
<div class="save-api-popup__copy-area__key-area">
<p class="save-api-popup__copy-area__key-area__key">{{ apiKeySecret }}</p>
</div>
<p class="save-api-popup__copy-area__copy-button" @click="onCopyClick">Copy</p>
</div>
<div class="save-api-popup__next-step-area">
<span class="save-api-popup__next-step-area__label">Next Step:</span>
<a
class="save-api-popup__next-step-area__link"
href="https://documentation.tardigrade.io/getting-started/uploading-your-first-object/set-up-uplink-cli"
target="_blank"
rel="noopener noreferrer"
@click.self.stop="segmentTrack"
>
Set Up Uplink CLI
</a>
<VButton
label="Done"
width="156px"
height="40px"
:on-press="onCloseClick"
/>
</div>
<div class="blur-content"></div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import VButton from '@/components/common/VButton.vue';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
@Component({
components: {
VButton,
HeaderlessInput,
},
})
export default class ApiKeysCopyPopup extends Vue {
/**
* Indicates if component should be rendered.
*/
@Prop({default: false})
private readonly isPopupShown: boolean;
@Prop({default: ''})
private readonly apiKeySecret: string;
/**
* Closes popup.
*/
public onCloseClick(): void {
this.$emit('closePopup');
}
/**
* Copies api key secret to buffer.
*/
public onCopyClick(): void {
this.$copyText(this.apiKeySecret);
this.$notify.success('Key saved to clipboard');
}
/**
* Tracks if user checked uplink CLI docs.
*/
public segmentTrack(): void {
this.$segment.track(SegmentEvent.CLI_DOCS_VIEWED, {
email: this.$store.getters.user.email,
});
}
}
</script>
<style scoped lang="scss">
.save-api-popup {
padding: 32px 40px;
background-color: #fff;
border-radius: 24px;
margin-top: 29px;
max-width: 94.8%;
height: auto;
position: relative;
font-family: 'font_regular', sans-serif;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 29px;
margin-bottom: 26px;
}
&__copy-area {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #f5f6fa;
padding: 29px 32px 29px 24px;
border-radius: 12px;
position: relative;
margin-bottom: 20px;
&__key-area {
&__key {
margin: 0;
font-size: 16px;
line-height: 21px;
word-break: break-all;
}
}
&__copy-button {
padding: 11px 22px;
margin: 0 0 0 20px;
background: #fff;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
color: #2683ff;
&:hover {
color: #fff;
background-color: #2683ff;
}
}
}
&__next-step-area {
display: flex;
justify-content: flex-end;
align-items: center;
width: 100%;
&__label {
font-size: 15px;
line-height: 49px;
letter-spacing: -0.100741px;
color: #a0a0a0;
margin-right: 15px;
}
&__link {
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #2683ff;
border-radius: 6px;
width: 154px;
height: 38px;
font-size: 15px;
line-height: 22px;
color: #2683ff;
margin-right: 15px;
&:hover {
color: #fff;
background-color: #2683ff;
}
}
}
.blur-content {
position: absolute;
top: 100%;
left: 0;
background-color: #f5f6fa;
width: 100%;
height: 70.5vh;
z-index: 100;
opacity: 0.3;
}
}
</style>

View File

@ -1,172 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="new-api-key" v-if="isPopupShown">
<h2 class="new-api-key__title">Name Your API Key</h2>
<HeaderlessInput
@setData="onChangeName"
:error="errorMessage"
placeholder="Enter API Key Name"
class="full-input"
width="100%"
/>
<VButton
class="next-button"
label="Next >"
width="128px"
height="48px"
:on-press="onNextClick"
/>
<div class="new-api-key__close-cross-container" @click="onCloseClick">
<CloseCrossIcon/>
</div>
<div class="blur-content"></div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import VButton from '@/components/common/VButton.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import { API_KEYS_ACTIONS } from '@/store/modules/apiKeys';
import { ApiKey } from '@/types/apiKeys';
import { SegmentEvent } from '@/utils/constants/analyticsEventNames';
const {
CREATE,
FETCH,
} = API_KEYS_ACTIONS;
@Component({
components: {
HeaderlessInput,
VButton,
CloseCrossIcon,
},
})
export default class ApiKeysCreationPopup extends Vue {
/**
* Indicates if component should be rendered.
*/
@Prop({default: false})
private readonly isPopupShown: boolean;
private name: string = '';
private errorMessage: string = '';
private isLoading: boolean = false;
private key: string = '';
private FIRST_PAGE = 1;
public onChangeName(value: string): void {
this.name = value.trim();
this.errorMessage = '';
}
public onCloseClick(): void {
this.onChangeName('');
this.$emit('closePopup');
}
/**
* Creates api key.
*/
public async onNextClick(): Promise<void> {
if (this.isLoading) {
return;
}
if (!this.name) {
this.errorMessage = 'API Key name can`t be empty';
return;
}
this.isLoading = true;
let createdApiKey: ApiKey;
try {
createdApiKey = await this.$store.dispatch(CREATE, this.name);
} catch (error) {
await this.$notify.error(error.message);
this.isLoading = false;
return;
}
await this.$notify.success('Successfully created new api key');
this.key = createdApiKey.secret;
this.isLoading = false;
this.name = '';
this.$segment.track(SegmentEvent.API_KEY_CREATED, {
project_id: this.$store.getters.selectedProject.id,
});
try {
await this.$store.dispatch(FETCH, this.FIRST_PAGE);
} catch (error) {
await this.$notify.error(`Unable to fetch API keys. ${error.message}`);
}
this.$emit('closePopup');
this.$emit('showCopyPopup', this.key);
}
}
</script>
<style scoped lang="scss">
.new-api-key {
padding: 32px 58px 41px 40px;
background-color: #fff;
border-radius: 24px;
margin-top: 29px;
max-width: 93.5%;
height: auto;
position: relative;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 29px;
margin-bottom: 26px;
}
.next-button {
margin-top: 20px;
}
&__close-cross-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
right: 29px;
top: 29px;
height: 24px;
width: 24px;
cursor: pointer;
&:hover .close-cross-svg-path {
fill: #2683ff;
}
}
.blur-content {
position: absolute;
top: 100%;
left: 0;
background-color: #f5f6fa;
width: 100%;
height: 70.5vh;
z-index: 100;
opacity: 0.3;
}
}
</style>

View File

@ -1,145 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="apikey-item-container">
<div class="apikey-item-container__common-info">
<div class="checkbox-container">
<CheckboxIcon class="checkbox-container__image"/>
</div>
<div class="avatar">
<AvatarIcon class="avatar__image"/>
</div>
<div class="name-container" :title="itemData.name">
<p class="name">{{ itemData.name }}</p>
</div>
</div>
<div class="apikey-item-container__common-info date-item-container">
<p class="date">{{ itemData.localDate() }}</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import AvatarIcon from '@/../static/images/apiKeys/avatar.svg';
import CheckboxIcon from '@/../static/images/common/checkbox.svg';
import { ApiKey } from '@/types/apiKeys';
@Component({
components: {
CheckboxIcon,
AvatarIcon,
},
})
export default class ApiKeysItem extends Vue {
@Prop({default: () => new ApiKey('', '', new Date(), '')})
private readonly itemData: ApiKey;
}
</script>
<style scoped lang="scss">
.apikey-item-container {
display: flex;
align-items: center;
justify-content: flex-start;
height: 83px;
background-color: #fff;
cursor: pointer;
width: 100%;
&:hover {
background-color: rgba(255, 255, 255, 0.5);
}
&__common-info {
display: flex;
align-items: center;
justify-content: flex-start;
width: 60%;
}
}
.checkbox-container {
margin-left: 28px;
max-height: 23px;
border-radius: 4px;
}
.avatar {
min-width: 40px;
max-width: 40px;
min-height: 40px;
max-height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-left: 25px;
}
.name-container {
max-width: calc(100% - 131px);
margin-right: 15px;
}
.name {
font-family: 'font_bold', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin-left: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.date {
font-family: 'font_regular', sans-serif;
font-size: 16px;
line-height: 21px;
color: #354049;
margin: 0;
}
.apikey-item-container.selected {
background-color: #2683ff;
.apikey-item-container__common-info {
.checkbox-container {
background-image: url('../../../static/images/apiKeys/Vector.png');
background-repeat: no-repeat;
background-size: 18px 12px;
background-position: center;
background-color: #fff;
&__image {
&__rect {
stroke: #fff;
}
}
}
.avatar {
&__image {
border: 1px solid #fff;
box-sizing: border-box;
border-radius: 6px;
}
}
}
.name,
.date {
color: #fff;
}
}
.date-item-container {
width: 40%;
}
</style>

View File

@ -1,225 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="no-api-keys-area">
<h1 class="no-api-keys-area__title">Create Your First API Key</h1>
<p class="no-api-keys-area__sub-title">API keys give access to the project to create buckets, upload objects</p>
<VButton
class="no-api-keys-area__button"
width="180px"
height="48px"
label="Create API Key"
:on-press="onButtonClick"
/>
<div class="no-api-keys-area__steps-area">
<div class="no-api-keys-area__steps-area__numbers">
<FirstStepIcon class="no-api-keys-area__steps-area__numbers__icon"/>
<div class="no-api-keys-area-divider"></div>
<SecondStepIcon class="no-api-keys-area__steps-area__numbers__icon"/>
<div class="no-api-keys-area-divider"></div>
<ThirdStepIcon class="no-api-keys-area__steps-area__numbers__icon"/>
</div>
<div class="no-api-keys-area__steps-area__items">
<div class="no-api-keys-area__steps-area__items__create-api-key">
<h2 class="no-api-keys-area__steps-area__items__create-api-key__title">Create & Save API Key</h2>
<img class="no-api-keys-area-image" src="@/../static/images/apiKeys/noApiKeysArea/apiKey.jpg" alt="api key image">
</div>
<div class="no-api-keys-area__steps-area__items__setup-uplink">
<h2 class="no-api-keys-area__steps-area__items__setup-uplink__title">Setup Uplink CLI</h2>
<img class="no-api-keys-area-image" src="@/../static/images/apiKeys/noApiKeysArea/uplink.jpg" alt="setup uplink image">
</div>
<div class="no-api-keys-area__steps-area__items__store-data">
<h2 class="no-api-keys-area__steps-area__items__store-data__title">Store Data</h2>
<img class="no-api-keys-area-image" src="@/../static/images/apiKeys/noApiKeysArea/store.jpg" alt="store data image">
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import VButton from '@/components/common/VButton.vue';
import FirstStepIcon from '@/../static/images/common/one.svg';
import ThirdStepIcon from '@/../static/images/common/three.svg';
import SecondStepIcon from '@/../static/images/common/two.svg';
@Component({
components: {
VButton,
FirstStepIcon,
SecondStepIcon,
ThirdStepIcon,
},
})
export default class NoApiKeysArea extends Vue {
@Prop({default: () => { return; }})
public readonly onButtonClick: Function;
}
</script>
<style scoped lang="scss">
h1,
h2,
p,
a {
margin: 0;
}
.no-api-keys-area {
width: auto;
height: calc(100% - 130px);
padding: 65px 65px 0 65px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: 'font_regular', sans-serif;
&__title {
font-size: 32px;
line-height: 39px;
color: #1b2533;
margin-bottom: 25px;
font-family: 'font_bold', sans-serif;
}
&__sub-title {
font-size: 16px;
line-height: 21px;
color: #354049;
margin-bottom: 38px;
}
&__button {
margin-bottom: 65px;
}
&__steps-area {
padding: 55px;
width: auto;
background-color: #fff;
border-radius: 12px;
&__numbers {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 0 125px;
margin-bottom: 15px;
&__icon {
min-width: 30px;
min-height: 30px;
}
}
&__items {
display: flex;
justify-content: space-between;
align-items: center;
&__create-api-key,
&__setup-uplink,
&__store-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
&__title {
font-family: 'font_bold', sans-serif;
font-size: 24px;
line-height: 16px;
text-align: center;
color: #354049;
margin: 25px 0;
}
}
&__setup-uplink,
&__create-api-key {
margin-right: 20px;
}
}
}
}
.no-api-keys-area-divider {
width: calc(46% + 2px);
border: 1px solid #cfd5da;
}
@media screen and (max-width: 1590px) {
.no-api-keys-area {
padding: 65px 0;
&__steps-area {
&__numbers {
padding: 0 95px;
}
&__items {
&__create-api-key,
&__setup-uplink,
&__store-data {
&__title {
font-family: 'font_bold', sans-serif;
font-size: 19px;
line-height: 16px;
text-align: center;
color: #354049;
}
}
}
}
}
.no-api-keys-area-image {
max-width: 230px;
max-height: 170px;
}
}
@media screen and (max-width: 900px) {
.no-api-keys-area {
&__steps-area {
&__numbers {
padding: 0 75px;
}
&__items {
&__create-api-key,
&__setup-uplink,
&__store-data {
&__title {
font-family: 'font_bold', sans-serif;
font-size: 14px;
line-height: 16px;
text-align: center;
color: #354049;
}
}
}
}
}
.no-api-keys-area-image {
max-width: 200px;
max-height: 140px;
}
}
</style>

View File

@ -1,116 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="sort-header-container">
<div class="sort-header-container__name-item" @click="onHeaderItemClick(ApiKeyOrderBy.NAME)">
<p class="sort-header-container__name-item__title">Key Name</p>
<VerticalArrows
:is-active="areApiKeysSortedByName"
:direction="getSortDirection"
/>
</div>
<div class="sort-header-container__date-item" @click="onHeaderItemClick(ApiKeyOrderBy.CREATED_AT)">
<p class="sort-header-container__date-item__title creation-date">Created</p>
<VerticalArrows
:is-active="!areApiKeysSortedByName"
:direction="getSortDirection"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import VerticalArrows from '@/components/common/VerticalArrows.vue';
import { ApiKeyOrderBy, OnHeaderClickCallback } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
@Component({
components: {
VerticalArrows,
},
})
export default class SortApiKeysHeader extends Vue {
@Prop({default: () => new Promise(() => false)})
private readonly onHeaderClickCallback: OnHeaderClickCallback;
public ApiKeyOrderBy = ApiKeyOrderBy;
public sortBy: ApiKeyOrderBy = ApiKeyOrderBy.NAME;
public sortDirection: SortDirection = SortDirection.ASCENDING;
/**
* Used for arrow styling.
*/
public get getSortDirection(): SortDirection {
return this.sortDirection === SortDirection.DESCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
}
public get areApiKeysSortedByName(): boolean {
return this.sortBy === ApiKeyOrderBy.NAME;
}
/**
* Sets sorting kind if different from current.
* If same, changes sort direction.
* @param sortBy
*/
public async onHeaderItemClick(sortBy: ApiKeyOrderBy): Promise<void> {
if (this.sortBy !== sortBy) {
this.sortBy = sortBy;
this.sortDirection = SortDirection.ASCENDING;
await this.onHeaderClickCallback(this.sortBy, this.sortDirection);
return;
}
this.sortDirection = this.sortDirection === SortDirection.DESCENDING ?
SortDirection.ASCENDING
: SortDirection.DESCENDING;
await this.onHeaderClickCallback(this.sortBy, this.sortDirection);
}
}
</script>
<style scoped lang="scss">
.sort-header-container {
display: flex;
width: 100%;
height: 40px;
background-color: rgba(255, 255, 255, 0.3);
margin-top: 31px;
&__name-item,
&__date-item {
width: 60%;
display: flex;
align-items: center;
margin: 0;
cursor: pointer;
&__title {
font-family: 'font_medium', sans-serif;
font-size: 16px;
margin: 0 0 0 80px;
color: #2a2a32;
}
.creation-date {
margin-left: 0;
}
}
&__date-item {
width: 40%;
&__title {
margin: 0;
}
}
}
</style>

View File

@ -19,7 +19,25 @@
:style="style.inputStyle"
@focus="showPasswordStrength"
@blur="hidePasswordStrength"
@click="showOptions"
:optionsShown="optionsShown"
@optionsList="optionsList"
/>
<!-- Shown if there are input choice options -->
<InputCaret v-if="optionsList.length > 0" class="headerless-input__caret" />
<ul v-click-outside="hideOptions" class="headerless-input__options-wrapper" v-if="optionsShown">
<li
class="headerless-input__option"
@click="chooseOption(option)"
v-for="(option, index) in optionsList"
:key="index"
>
{{option}}
</li>
</ul>
<!-- end of option render logic-->
<!--2 conditions of eye image (crossed or not) -->
<PasswordHiddenIcon
class="input-wrap__image"
@ -38,6 +56,7 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import InputCaret from '@/../static/images/common/caret.svg';
import PasswordHiddenIcon from '@/../static/images/common/passwordHidden.svg';
import PasswordShownIcon from '@/../static/images/common/passwordShown.svg';
import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
@ -45,6 +64,7 @@ import ErrorIcon from '@/../static/images/register/ErrorInfo.svg';
// Custom input component for login page
@Component({
components: {
InputCaret,
ErrorIcon,
PasswordHiddenIcon,
PasswordShownIcon,
@ -73,6 +93,12 @@ export default class HeaderlessInput extends Vue {
protected readonly error: string;
@Prop({default: Number.MAX_SAFE_INTEGER})
protected readonly maxSymbols: number;
@Prop({default: []})
protected readonly optionsList: [string];
@Prop({default: false})
protected optionsShown: boolean;
@Prop({default: false})
protected inputClicked: boolean;
@Prop({default: false})
private readonly isWhite: boolean;
@ -120,6 +146,36 @@ export default class HeaderlessInput extends Vue {
this.type = this.isPasswordShown ? this.textType : this.passwordType;
}
/**
* Chose a dropdown option as the input value.
*/
public chooseOption(option: string): void {
this.value = option;
this.$emit('setData', this.value);
this.optionsShown = false;
}
/**
* Show dropdown options when the input is clicked, if they exist.
*/
public showOptions(): void {
if (this.optionsList.length > 0) {
this.optionsShown = true;
this.inputClicked = true;
}
}
/**
* Hide the dropdown options from view when there is a click outside of the dropdown.
*/
public hideOptions(): void {
if (this.optionsList.length > 0 && !this.inputClicked && this.optionsShown) {
this.optionsShown = false;
this.inputClicked = false;
}
this.inputClicked = false;
}
public get isLabelShown(): boolean {
return !!(!this.error && this.label);
}
@ -170,6 +226,72 @@ export default class HeaderlessInput extends Vue {
fill: #2683ff !important;
}
}
.headerless-input {
font-size: 16px;
line-height: 21px;
resize: none;
height: 46px;
padding: 0 30px 0 0;
width: calc(100% - 30px) !important;
text-indent: 20px;
border: 1px solid rgba(56, 75, 101, 0.4);
border-radius: 6px;
&__caret {
position: absolute;
right: 28px;
bottom: 18px;
}
&__options-wrapper {
border: 1px solid rgba(56, 75, 101, 0.4);
position: absolute;
width: 100%;
top: 89px;
padding: 0;
background: #fff;
z-index: 21;
border-radius: 6px;
list-style: none;
border-top-right-radius: 0;
border-top-left-radius: 0;
border-top: none;
height: 176px;
margin-top: 0;
}
&__option {
cursor: pointer;
padding: 20px 22px;
&:hover {
background: #2582ff;
color: #fff;
}
}
}
.headerless-input::placeholder {
color: #384b65;
opacity: 0.4;
}
&:focus-within {
.headerless-input {
position: relative;
z-index: 22;
&__options-wrapper {
border-top: 3px solid #145ecc;
}
&__caret {
z-index: 23;
}
}
}
}
.label-container {
@ -199,23 +321,6 @@ export default class HeaderlessInput extends Vue {
}
}
.headerless-input {
font-size: 16px;
line-height: 21px;
resize: none;
height: 46px;
padding: 0 30px 0 0;
width: calc(100% - 30px) !important;
text-indent: 20px;
border: 1px solid rgba(56, 75, 101, 0.4);
border-radius: 6px;
}
.headerless-input::placeholder {
color: #384b65;
opacity: 0.4;
}
.inputError::placeholder {
color: #eb5757;
opacity: 0.4;

View File

@ -164,7 +164,7 @@ export default class NewProjectPopup extends Vue {
await this.$notify.error(`Unable to create project. ${error.message}`);
}
this.clearApiKeys();
this.clearAccessGrants();
this.clearBucketUsage();
@ -197,9 +197,9 @@ export default class NewProjectPopup extends Vue {
}
/**
* Clears api keys store.
* Clears access grants store.
*/
private clearApiKeys(): void {
private clearAccessGrants(): void {
this.$store.dispatch(ACCESS_GRANTS_ACTIONS.CLEAR);
}

View File

@ -5,7 +5,6 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { AccessGrantsApiGql } from '@/api/accessGrants';
import { ApiKeysApiGql } from '@/api/apiKeys';
import { AuthHttpApi } from '@/api/auth';
import { BucketsApiGql } from '@/api/buckets';
import { PaymentsHttpApi } from '@/api/payments';
@ -13,7 +12,6 @@ import { ProjectMembersApiGql } from '@/api/projectMembers';
import { ProjectsApiGql } from '@/api/projects';
import { notProjectRelatedRoutes, router } from '@/router';
import { AccessGrantsState, makeAccessGrantsModule } from '@/store/modules/accessGrants';
import { ApiKeysState, makeApiKeysModule } from '@/store/modules/apiKeys';
import { appStateModule } from '@/store/modules/appState';
import { makeBucketsModule } from '@/store/modules/buckets';
import { makeNotificationsModule, NotificationsState } from '@/store/modules/notifications';
@ -34,7 +32,6 @@ export class StoreModule<S> {
// TODO: remove it after we will use modules as classes and use some DI framework
const authApi = new AuthHttpApi();
const apiKeysApi = new ApiKeysApiGql();
const accessGrantsApi = new AccessGrantsApiGql();
const bucketsApi = new BucketsApiGql();
const projectMembersApi = new ProjectMembersApiGql();
@ -43,7 +40,6 @@ const paymentsApi = new PaymentsHttpApi();
class ModulesState {
public notificationsModule: NotificationsState;
public apiKeysModule: ApiKeysState;
public accessGrantsModule: AccessGrantsState;
public appStateModule;
public projectMembersModule: ProjectMembersState;
@ -56,7 +52,6 @@ class ModulesState {
export const store = new Vuex.Store<ModulesState>({
modules: {
notificationsModule: makeNotificationsModule(),
apiKeysModule: makeApiKeysModule(apiKeysApi),
accessGrantsModule: makeAccessGrantsModule(accessGrantsApi),
appStateModule,
projectMembersModule: makeProjectMembersModule(projectMembersApi),

View File

@ -1,155 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { StoreModule } from '@/store';
import { ApiKey, ApiKeyCursor, ApiKeyOrderBy, ApiKeysApi, ApiKeysPage } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
export const API_KEYS_ACTIONS = {
FETCH: 'fetchApiKeys',
CREATE: 'createApiKey',
DELETE: 'deleteApiKey',
CLEAR: 'clearApiKeys',
SET_SEARCH_QUERY: 'setApiKeysSearchQuery',
SET_SORT_BY: 'setApiKeysSortingBy',
SET_SORT_DIRECTION: 'setApiKeysSortingDirection',
TOGGLE_SELECTION: 'toggleApiKeySelection',
CLEAR_SELECTION: 'clearApiKeySelection',
};
export const API_KEYS_MUTATIONS = {
SET_PAGE: 'setApiKeys',
TOGGLE_SELECTION: 'toggleApiKeysSelection',
CLEAR_SELECTION: 'clearApiKeysSelection',
CLEAR: 'clearApiKeys',
CHANGE_SORT_ORDER: 'changeApiKeysSortOrder',
CHANGE_SORT_ORDER_DIRECTION: 'changeApiKeysSortOrderDirection',
SET_SEARCH_QUERY: 'setApiKeysSearchQuery',
SET_PAGE_NUMBER: 'setApiKeysPage',
};
const {
SET_PAGE,
TOGGLE_SELECTION,
CLEAR_SELECTION,
CLEAR,
CHANGE_SORT_ORDER,
CHANGE_SORT_ORDER_DIRECTION,
SET_SEARCH_QUERY,
SET_PAGE_NUMBER,
} = API_KEYS_MUTATIONS;
export class ApiKeysState {
public cursor: ApiKeyCursor = new ApiKeyCursor();
public page: ApiKeysPage = new ApiKeysPage();
public selectedApiKeysIds: string[] = [];
}
/**
* creates apiKeys module with all dependencies
*
* @param api - apiKeys api
*/
export function makeApiKeysModule(api: ApiKeysApi): StoreModule<ApiKeysState> {
return {
state: new ApiKeysState(),
mutations: {
[SET_PAGE](state: ApiKeysState, page: ApiKeysPage) {
state.page = page;
state.page.apiKeys = state.page.apiKeys.map(apiKey => {
if (state.selectedApiKeysIds.includes(apiKey.id)) {
apiKey.isSelected = true;
}
return apiKey;
});
},
[SET_PAGE_NUMBER](state: ApiKeysState, pageNumber: number) {
state.cursor.page = pageNumber;
},
[SET_SEARCH_QUERY](state: ApiKeysState, search: string) {
state.cursor.search = search;
},
[CHANGE_SORT_ORDER](state: ApiKeysState, order: ApiKeyOrderBy) {
state.cursor.order = order;
},
[CHANGE_SORT_ORDER_DIRECTION](state: ApiKeysState, direction: SortDirection) {
state.cursor.orderDirection = direction;
},
[TOGGLE_SELECTION](state: ApiKeysState, apiKey: ApiKey) {
if (!state.selectedApiKeysIds.includes(apiKey.id)) {
apiKey.isSelected = true;
state.selectedApiKeysIds.push(apiKey.id);
return;
}
apiKey.isSelected = false;
state.selectedApiKeysIds = state.selectedApiKeysIds.filter(apiKeyId => {
return apiKey.id !== apiKeyId;
});
},
[CLEAR_SELECTION](state: ApiKeysState) {
state.selectedApiKeysIds = [];
state.page.apiKeys = state.page.apiKeys.map((apiKey: ApiKey) => {
apiKey.isSelected = false;
return apiKey;
});
},
[CLEAR](state: ApiKeysState) {
state.cursor = new ApiKeyCursor();
state.page = new ApiKeysPage();
state.selectedApiKeysIds = [];
},
},
actions: {
fetchApiKeys: async function ({commit, rootGetters, state}, pageNumber: number): Promise<ApiKeysPage> {
const projectId = rootGetters.selectedProject.id;
commit(SET_PAGE_NUMBER, pageNumber);
const apiKeysPage: ApiKeysPage = await api.get(projectId, state.cursor);
commit(SET_PAGE, apiKeysPage);
return apiKeysPage;
},
createApiKey: async function ({commit, rootGetters}: any, name: string): Promise<ApiKey> {
const projectId = rootGetters.selectedProject.id;
const apiKey = await api.create(projectId, name);
return apiKey;
},
deleteApiKey: async function({state, commit}: any): Promise<void> {
await api.delete(state.selectedApiKeysIds);
commit(CLEAR_SELECTION);
},
setApiKeysSearchQuery: function ({commit}, search: string) {
commit(SET_SEARCH_QUERY, search);
},
setApiKeysSortingBy: function ({commit}, order: ApiKeyOrderBy) {
commit(CHANGE_SORT_ORDER, order);
},
setApiKeysSortingDirection: function ({commit}, direction: SortDirection) {
commit(CHANGE_SORT_ORDER_DIRECTION, direction);
},
toggleApiKeySelection: function ({commit}, apiKey: ApiKey): void {
commit(TOGGLE_SELECTION, apiKey);
},
clearApiKeySelection: function ({commit}): void {
commit(CLEAR_SELECTION);
},
clearApiKeys: function ({commit}): void {
commit(CLEAR);
commit(CLEAR_SELECTION);
},
},
getters: {
selectedApiKeys: (state: ApiKeysState) => state.page.apiKeys.filter((key: ApiKey) => key.isSelected),
apiKeys: function (state: ApiKeysState): ApiKey[] {
return state.page.apiKeys;
},
},
};
}

View File

@ -28,19 +28,15 @@ export const appStateModule = {
isChangePasswordPopupShown: false,
isPaymentSelectionShown: false,
isCreateProjectButtonShown: false,
isSaveApiKeyModalShown: false,
},
satelliteName: '',
isBetaSatellite: false,
},
mutations: {
// Mutation changing add projectMembers members popup visibility
[APP_STATE_MUTATIONS.TOGGLE_ADD_TEAMMEMBER_POPUP](state: any): void {
state.appState.isAddTeamMembersPopupShown = !state.appState.isAddTeamMembersPopupShown;
},
// Mutation changing save api key modal visibility
[APP_STATE_MUTATIONS.TOGGLE_SAVE_API_KEY_MODAL](state: any): void {
state.appState.isSaveApiKeyModalShown = !state.appState.isSaveApiKeyModalShown;
},
// Mutation changing account dropdown visibility
[APP_STATE_MUTATIONS.TOGGLE_ACCOUNT_DROPDOWN](state: any): void {
state.appState.isAccountDropdownShown = !state.appState.isAccountDropdownShown;
@ -127,9 +123,12 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_PAYMENT_SELECTION](state: any, value: boolean): void {
state.appState.isPaymentSelectionShown = value;
},
[APP_STATE_MUTATIONS.SET_NAME](state: any, satelliteName: string): void {
[APP_STATE_MUTATIONS.SET_SATELLITE_NAME](state: any, satelliteName: string): void {
state.satelliteName = satelliteName;
},
[APP_STATE_MUTATIONS.SET_SATELLITE_STATUS](state: any, isBetaSatellite: boolean): void {
state.isBetaSatellite = isBetaSatellite;
},
},
actions: {
// Commits mutation for changing app popups and dropdowns visibility state
@ -140,13 +139,6 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.TOGGLE_ADD_TEAMMEMBER_POPUP);
},
[APP_STATE_ACTIONS.TOGGLE_SAVE_API_KEY_MODAL]: function ({commit, state}: any): void {
if (!state.appState.isSaveApiKeyModalShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
}
commit(APP_STATE_MUTATIONS.TOGGLE_SAVE_API_KEY_MODAL);
},
[APP_STATE_ACTIONS.TOGGLE_ACCOUNT]: function ({commit, state}: any): void {
if (!state.appState.isAccountDropdownShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
@ -271,7 +263,10 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.CHANGE_STATE, newFetchState);
},
[APP_STATE_ACTIONS.SET_SATELLITE_NAME]: function ({commit}: any, satelliteName: string): void {
commit(APP_STATE_MUTATIONS.SET_NAME, satelliteName);
commit(APP_STATE_MUTATIONS.SET_SATELLITE_NAME, satelliteName);
},
[APP_STATE_ACTIONS.SET_SATELLITE_STATUS]: function ({commit}: any, isBetaSatellite: boolean): void {
commit(APP_STATE_MUTATIONS.SET_SATELLITE_STATUS, isBetaSatellite);
},
},
};

View File

@ -211,7 +211,7 @@ export function makeProjectsModule(api: ProjectsApi): StoreModule<ProjectsState>
projectsCount: (state: ProjectsState, getters: any): number => {
let projectsCount: number = 0;
state.projects.map((project: Project) => {
state.projects.forEach((project: Project) => {
if (project.ownerId === getters.user.id) {
projectsCount++;
}

View File

@ -16,7 +16,6 @@ export const APP_STATE_MUTATIONS = {
TOGGLE_RESOURCES_DROPDOWN: 'TOGGLE_RESOURCES_DROPDOWN',
TOGGLE_SETTINGS_DROPDOWN: 'TOGGLE_SETTINGS_DROPDOWN',
TOGGLE_EDIT_PROJECT_DROPDOWN: 'TOGGLE_EDIT_PROJECT_DROPDOWN',
TOGGLE_SAVE_API_KEY_MODAL: 'TOGGLE_SAVE_API_KEY_MODAL',
TOGGLE_DELETE_PROJECT_DROPDOWN: 'TOGGLE_DELETE_PROJECT_DROPDOWN',
TOGGLE_DELETE_ACCOUNT_DROPDOWN: 'TOGGLE_DELETE_ACCOUNT_DROPDOWN',
TOGGLE_FREE_CREDITS_DROPDOWN: 'TOGGLE_FREE_CREDITS_DROPDOWN',
@ -32,7 +31,8 @@ export const APP_STATE_MUTATIONS = {
CLOSE_ALL: 'CLOSE_ALL',
CHANGE_STATE: 'CHANGE_STATE',
TOGGLE_PAYMENT_SELECTION: 'TOGGLE_PAYMENT_SELECTION',
SET_NAME: 'SET_NAME',
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
SHOW_CREATE_PROJECT_BUTTON: 'SHOW_CREATE_PROJECT_BUTTON',
HIDE_CREATE_PROJECT_BUTTON: 'HIDE_CREATE_PROJECT_BUTTON',
};

View File

@ -1,95 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { SortDirection } from '@/types/common';
export type OnHeaderClickCallback = (sortBy: ApiKeyOrderBy, sortDirection: SortDirection) => Promise<void>;
/**
* Exposes all apiKey-related functionality.
*/
export interface ApiKeysApi {
/**
* Fetch apiKeys
*
* @returns ApiKey[]
* @throws Error
*/
get(projectId: string, cursor: ApiKeyCursor): Promise<ApiKeysPage>;
/**
* Create new apiKey
*
* @returns ApiKey
* @throws Error
*/
create(projectId: string, name: string): Promise<ApiKey>;
/**
* Delete existing apiKey
*
* @returns null
* @throws Error
*/
delete(ids: string[]): Promise<void>;
}
/**
* Holds api keys sorting parameters.
*/
export enum ApiKeyOrderBy {
NAME = 1,
CREATED_AT,
}
/**
* ApiKeyCursor is a type, used to describe paged api keys list.
*/
export class ApiKeyCursor {
public constructor(
public search: string = '',
public limit: number = 6,
public page: number = 1,
public order: ApiKeyOrderBy = ApiKeyOrderBy.NAME,
public orderDirection: SortDirection = SortDirection.ASCENDING,
) {}
}
/**
* ApiKeysPage is a type, used to describe paged api keys list.
*/
export class ApiKeysPage {
public constructor(
public apiKeys: ApiKey[] = [],
public search: string = '',
public order: ApiKeyOrderBy = ApiKeyOrderBy.NAME,
public orderDirection: SortDirection = SortDirection.ASCENDING,
public limit: number = 6,
public pageCount: number = 0,
public currentPage: number = 1,
public totalCount: number = 0,
) {}
}
/**
* ApiKey class holds info for ApiKeys entity.
*/
export class ApiKey {
public isSelected: boolean;
constructor(
public id: string = '',
public name: string = '',
public createdAt: Date = new Date(),
public secret: string = '',
) {
this.isSelected = false;
}
/**
* Returns created date as a local date string.
*/
public localDate(): string {
return this.createdAt.toLocaleDateString();
}
}

View File

@ -34,6 +34,10 @@ export class User {
public partnerId: string = '',
public password: string = '',
public projectLimit: number = 0,
public isProfessional: boolean = false,
public position: string = '',
public companyName: string = '',
public employeeCount: string = '',
) {}
public getFullName(): string {

View File

@ -3,7 +3,6 @@
export const APP_STATE_ACTIONS = {
TOGGLE_TEAM_MEMBERS: 'toggleAddTeamMembersPopup',
TOGGLE_SAVE_API_KEY_MODAL: 'toggleSaveApiKeyModal',
TOGGLE_ACCOUNT: 'toggleAccountDropdown',
TOGGLE_SELECT_PROJECT_DROPDOWN: 'toggleSelectProjectDropdown',
TOGGLE_RESOURCES_DROPDOWN: 'toggleResourcesDropdown',
@ -27,6 +26,7 @@ export const APP_STATE_ACTIONS = {
CHANGE_STATE: 'changeFetchState',
TOGGLE_PAYMENT_SELECTION: 'TOGGLE_PAYMENT_SELECTION',
SET_SATELLITE_NAME: 'SET_SATELLITE_NAME',
SET_SATELLITE_STATUS: 'SET_SATELLITE_STATUS',
SHOW_CREATE_PROJECT_BUTTON: 'SHOW_CREATE_PROJECT_BUTTON',
HIDE_CREATE_PROJECT_BUTTON: 'HIDE_CREATE_PROJECT_BUTTON',
};

View File

@ -6,7 +6,12 @@
<div v-if="isLoading" class="loading-overlay active">
<img class="loading-image" src="@/../static/images/register/Loading.gif" alt="Company logo loading gif">
</div>
<NoPaywallInfoBar v-if="isNoPaywallInfoBarShown && !isLoading"/>
<div v-if="isBetaSatellite" class="dashboard__beta-banner">
<p class="dashboard__beta-banner__message">
Please be aware that this is a beta satellite. Data uploaded may be deleted at any point in time.
</p>
</div>
<NoPaywallInfoBar v-if="isNoPaywallInfoBarShown && !isLoading && !isBetaSatellite"/>
<div v-if="!isLoading" class="dashboard__wrap">
<DashboardHeader/>
<div class="dashboard__wrap__main-area">
@ -234,6 +239,13 @@ export default class DashboardArea extends Vue {
this.$store.state.paymentsModule.creditCards.length === 0;
}
/**
* Indicates if satellite is in beta.
*/
public get isBetaSatellite(): boolean {
return this.$store.state.appStateModule.isBetaSatellite;
}
/**
* Indicates if billing info bar is shown.
*/
@ -381,6 +393,23 @@ export default class DashboardArea extends Vue {
display: flex;
flex-direction: column;
&__beta-banner {
width: calc(100% - 60px);
padding: 0 30px;
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'font_regular', sans-serif;
background-color: red;
&__message {
font-weight: normal;
font-size: 14px;
line-height: 12px;
color: #fff;
}
}
&__wrap {
display: flex;
flex-direction: column;

View File

@ -55,8 +55,12 @@ export default class RegisterArea extends Vue {
private emailError: string = '';
private passwordError: string = '';
private repeatedPasswordError: string = '';
private companyNameError: string = '';
private employeeCountError: string = '';
private positionError: string = '';
private isTermsAcceptedError: boolean = false;
private isLoading: boolean = false;
private isProfessional: boolean = false;
// Only for beta sats (like US2).
private areBetaTermsAcceptedError: boolean = false;
@ -68,6 +72,10 @@ export default class RegisterArea extends Vue {
// tardigrade logic
public isDropdownShown: boolean = false;
// Employee Count dropdown options
public employeeCountOptions = ['1-50', '51-1000', '1001+'];
public optionsShown = false;
/**
* Lifecycle hook before vue instance is created.
* Initializes google tag manager (Tardigrade).
@ -161,7 +169,6 @@ export default class RegisterArea extends Vue {
return;
}
await this.createUser();
this.isLoading = false;
@ -215,14 +222,41 @@ export default class RegisterArea extends Vue {
}
/**
* Only for US2 tardigrade beta satellite.
* Sets user's repeat password field from value string.
* Indicates if satellite is in beta.
*/
public get isBetaSatellite(): boolean {
const US2_SAT_NAME_PART = 'US2';
const satName: string = this.$store.state.appStateModule.satelliteName;
return this.$store.state.appStateModule.isBetaSatellite;
}
return satName.includes(US2_SAT_NAME_PART);
/**
* Sets user's company name field from value string.
*/
public setCompanyName(value: string): void {
this.user.companyName = value.trim();
this.companyNameError = '';
}
/**
* Sets user's company size field from value string.
*/
public setEmployeeCount(value: string): void {
this.user.employeeCount = value;
this.employeeCountError = '';
}
/**
* Sets user's position field from value string.
*/
public setPosition(value: string): void {
this.user.position = value.trim();
this.positionError = '';
}
/**
* toggle user account type
*/
public toggleAccountType(value: boolean): void {
this.isProfessional = value;
}
/**
@ -246,6 +280,25 @@ export default class RegisterArea extends Vue {
isNoErrors = false;
}
if (this.isProfessional) {
if (!this.user.companyName.trim()) {
this.companyNameError = 'No Company Name filled in';
isNoErrors = false;
}
if (!this.user.position.trim()) {
this.positionError = 'No Position filled in';
isNoErrors = false;
}
if (!this.user.employeeCount.trim()) {
this.employeeCountError = 'No Company Size filled in';
isNoErrors = false;
}
}
if (this.repeatedPassword !== this.password) {
this.repeatedPasswordError = 'Password doesn\'t match';
isNoErrors = false;
@ -269,14 +322,23 @@ export default class RegisterArea extends Vue {
* Creates user and toggles successful registration area visibility.
*/
private async createUser(): Promise<void> {
this.user.isProfessional = this.isProfessional;
try {
this.userId = await this.auth.register(this.user, this.secret);
LocalData.setUserId(this.userId);
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
});
if (this.user.isProfessional) {
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
position: this.$store.getters.user.position,
company_name: this.$store.getters.user.companyName,
employee_count: this.$store.getters.user.employeeCount,
});
} else {
this.$segment.identify(this.userId, {
email: this.$store.getters.user.email,
});
}
const verificationPageURL: string = MetaUtils.getMetaContent('verification-page-url');
if (verificationPageURL) {
@ -289,7 +351,6 @@ export default class RegisterArea extends Vue {
return;
}
await this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_REGISTRATION);
} catch (error) {
await this.$notify.error(error.message);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

View File

@ -1,6 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 6C0 2.68629 2.68629 0 6 0H24C27.3137 0 30 2.68629 30 6V24C30 27.3137 27.3137 30 24 30H6C2.68629 30 0 27.3137 0 24V6Z" fill="#2683FF"/>
<path d="M11.9184 11C13.4286 11 14.7239 11.8848 15.3777 13.1667H22.0408C22.1155 13.1667 22.1875 13.1921 22.2449 13.2396L23.8776 14.5886C23.9573 14.6537 24.0026 14.7533 23.9994 14.8581C23.9968 14.9623 23.9458 15.0593 23.8623 15.1198L21.5766 16.7708C21.456 16.8581 21.2928 16.8516 21.1786 16.7552L20.4082 16.099L19.6378 16.7552C19.5172 16.8581 19.3406 16.8581 19.2194 16.7552L18.449 16.099L17.6786 16.7552C17.6205 16.8053 17.5465 16.8327 17.4694 16.8333H15.3724C14.7174 18.112 13.426 19 11.9184 19C9.75837 19 8 17.205 8 15C8 12.795 9.75837 11 11.9184 11H11.9184Z" fill="white"/>
<path d="M10.7754 13.666C11.4928 13.666 12.0815 14.2669 12.0815 14.9993C12.0815 15.7318 11.4928 16.3327 10.7754 16.3327C10.0579 16.3327 9.46924 15.7318 9.46924 14.9993C9.46924 14.2669 10.0579 13.666 10.7754 13.666Z" fill="#2683FF"/>
<path d="M10.7756 14.334C10.4108 14.334 10.1226 14.6283 10.1226 15.0007C10.1226 15.3731 10.4108 15.6673 10.7756 15.6673C11.1404 15.6673 11.4287 15.3731 11.4287 15.0007C11.4287 14.6283 11.1404 14.334 10.7756 14.334Z" fill="#2683FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="15" height="7" viewBox="0 0 15 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 7L0.138785 0.25L14.8612 0.250001L7.5 7Z" fill="#384761"/>
</svg>

After

Width:  |  Height:  |  Size: 173 B

View File

@ -1,241 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import ApiKeysArea from '@/components/apiKeys/ApiKeysArea.vue';
import { PaymentsHttpApi } from '@/api/payments';
import { API_KEYS_MUTATIONS, makeApiKeysModule } from '@/store/modules/apiKeys';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makePaymentsModule } from '@/store/modules/payments';
import { makeProjectsModule } from '@/store/modules/projects';
import { ApiKey, ApiKeyOrderBy, ApiKeysPage } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
import { Project } from '@/types/projects';
import { NotificatorPlugin } from '@/utils/plugins/notificator';
import { SegmentioPlugin } from '@/utils/plugins/segment';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { ApiKeysMock } from '../mock/api/apiKeys';
import { ProjectsApiMock } from '../mock/api/projects';
const localVue = createLocalVue();
const segmentioPlugin = new SegmentioPlugin();
const notificationPlugin = new NotificatorPlugin();
localVue.use(Vuex);
localVue.use(segmentioPlugin);
localVue.use(notificationPlugin);
const apiKeysApi = new ApiKeysMock();
const apiKeysModule = makeApiKeysModule(apiKeysApi);
const projectsApi = new ProjectsApiMock();
const projectsModule = makeProjectsModule(projectsApi);
const paymentsApi = new PaymentsHttpApi();
const paymentsModule = makePaymentsModule(paymentsApi);
const notificationsModule = makeNotificationsModule();
const { CLEAR, SET_PAGE } = API_KEYS_MUTATIONS;
const store = new Vuex.Store({ modules: { projectsModule, apiKeysModule, paymentsModule, notificationsModule }});
describe('ApiKeysArea', (): void => {
const project = new Project('id', 'projectName', 'projectDescription', 'test', 'testOwnerId', true);
projectsApi.setMockProjects([project]);
const date = new Date(0);
const apiKey = new ApiKey('testId', 'test', date, 'test');
const apiKey1 = new ApiKey('testId1', 'test1', date, 'test1');
const testApiKeysPage = new ApiKeysPage([apiKey], '', ApiKeyOrderBy.NAME, SortDirection.ASCENDING, 6, 1, 1, 2);
apiKeysApi.setMockApiKeysPage(testApiKeysPage);
it('renders correctly', (): void => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper).toMatchSnapshot();
});
it('function apiKeyList works correctly', (): void => {
store.commit(SET_PAGE, testApiKeysPage);
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.apiKeyList).toEqual([apiKey]);
});
it('action on toggleSelection works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.toggleSelection(apiKey);
expect(store.getters.selectedApiKeys.length).toBe(1);
});
it('action on onClearSelection works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.onClearSelection();
expect(wrapper.vm.$data.isDeleteClicked).toBe(false);
});
it('function onCreateApiKeyClick works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.onCreateApiKeyClick();
expect(wrapper.vm.$data.isNewApiKeyPopupShown).toBe(true);
});
it('function onDeleteClick works correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.toggleSelection(apiKey);
await wrapper.find('.deletion').trigger('click');
expect(wrapper.vm.$data.isDeleteClicked).toBe(true);
setTimeout(async () => {
await wrapper.find('.deletion').trigger('click');
expect(wrapper.vm.$data.isDeleteClicked).toBe(false);
}, 1000);
await wrapper.vm.onClearSelection();
});
it('function apiKeyCountTitle works correctly', (): void => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.apiKeyCountTitle).toMatch('api key');
});
it('function isEmpty works correctly', (): void => {
store.commit(SET_PAGE, testApiKeysPage);
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.isEmpty).toBe(false);
});
it('function selectedAPIKeysCount works correctly', (): void => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.selectedAPIKeysCount).toBe(0);
});
it('function headerState works correctly', (): void => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.headerState).toBe(0);
});
it('function apiKeyCountTitle with 2 keys works correctly', (): void => {
const testPage = new ApiKeysPage();
testPage.apiKeys = [apiKey, apiKey1];
testPage.totalCount = 1;
testPage.pageCount = 1;
apiKeysApi.setMockApiKeysPage(testPage);
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper.vm.apiKeyCountTitle).toMatch('api keys');
});
it('function closeNewApiKeyPopup works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.closeNewApiKeyPopup();
expect(wrapper.vm.$data.isNewApiKeyPopupShown).toBe(false);
});
it('function showCopyApiKeyPopup works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
const testSecret = 'testSecret';
await wrapper.vm.showCopyApiKeyPopup(testSecret);
expect(wrapper.vm.$data.isCopyApiKeyPopupShown).toBe(true);
expect(wrapper.vm.$data.apiKeySecret).toMatch('testSecret');
});
it('function closeCopyNewApiKeyPopup works correctly', async (): Promise<void> => {
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
await wrapper.vm.closeCopyNewApiKeyPopup();
expect(wrapper.vm.$data.isCopyApiKeyPopupShown).toBe(false);
});
it('renders empty screen with add key prompt', (): void => {
store.commit(CLEAR);
const wrapper = mount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper).toMatchSnapshot();
});
it('renders empty search state correctly', (): void => {
const testPage = new ApiKeysPage();
testPage.apiKeys = [];
testPage.totalCount = 0;
testPage.pageCount = 0;
testPage.search = 'testSearch';
apiKeysApi.setMockApiKeysPage(testPage);
store.commit(SET_PAGE, testPage);
const wrapper = shallowMount(ApiKeysArea, {
store,
localVue,
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,52 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import VueClipboard from 'vue-clipboard2';
import Vuex from 'vuex';
import ApiKeysCopyPopup from '@/components/apiKeys/ApiKeysCopyPopup.vue';
import { ApiKeysApiGql } from '@/api/apiKeys';
import { makeApiKeysModule } from '@/store/modules/apiKeys';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { NotificatorPlugin } from '@/utils/plugins/notificator';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueClipboard);
const notificationPlugin = new NotificatorPlugin();
localVue.use(notificationPlugin);
const apiKeysApi = new ApiKeysApiGql();
const apiKeysModule = makeApiKeysModule(apiKeysApi);
const notificationsModule = makeNotificationsModule();
const testKey = 'test';
const store = new Vuex.Store({ modules: { notificationsModule, apiKeysModule }});
describe('ApiKeysCopyPopup', (): void => {
it('renders correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysCopyPopup, {
store,
localVue,
propsData: {
isPopupShown: true,
apiKeySecret: testKey,
},
});
expect(wrapper).toMatchSnapshot();
await expect(wrapper.find('.save-api-popup__copy-area__key-area__key').text()).toBe(testKey);
});
it('function onCloseClick works correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysCopyPopup, {
store,
localVue,
});
await wrapper.vm.onCloseClick();
expect(wrapper.emitted()).toEqual({'closePopup': [[]]});
});
});

View File

@ -1,115 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import ApiKeysCreationPopup from '@/components/apiKeys/ApiKeysCreationPopup.vue';
import { ApiKeysApiGql } from '@/api/apiKeys';
import { ProjectsApiGql } from '@/api/projects';
import { API_KEYS_ACTIONS, makeApiKeysModule } from '@/store/modules/apiKeys';
import { makeNotificationsModule } from '@/store/modules/notifications';
import { makeProjectsModule } from '@/store/modules/projects';
import { ApiKey } from '@/types/apiKeys';
import { Project } from '@/types/projects';
import { NotificatorPlugin } from '@/utils/plugins/notificator';
import { SegmentioPlugin } from '@/utils/plugins/segment';
import { createLocalVue, mount } from '@vue/test-utils';
const localVue = createLocalVue();
localVue.use(Vuex);
const notificationPlugin = new NotificatorPlugin();
const segmentioPlugin = new SegmentioPlugin();
localVue.use(notificationPlugin);
localVue.use(segmentioPlugin);
const apiKeysApi = new ApiKeysApiGql();
const apiKeysModule = makeApiKeysModule(apiKeysApi);
const projectsApi = new ProjectsApiGql();
const projectsModule = makeProjectsModule(projectsApi);
const notificationsModule = makeNotificationsModule();
const selectedProject = new Project();
selectedProject.id = '1';
projectsModule.state.selectedProject = selectedProject;
const CREATE = API_KEYS_ACTIONS.CREATE;
const store = new Vuex.Store({ modules: { projectsModule, apiKeysModule, notificationsModule }});
describe('ApiKeysCreationPopup', (): void => {
const value = 'testValue';
it('renders correctly', (): void => {
const wrapper = mount(ApiKeysCreationPopup, {
store,
localVue,
propsData: {
isPopupShown: true,
},
});
expect(wrapper).toMatchSnapshot();
});
it('function onCloseClick works correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysCreationPopup, {
store,
localVue,
});
await wrapper.vm.onCloseClick();
expect(wrapper.emitted()).toEqual({'closePopup': [[]]});
});
it('function onChangeName works correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysCreationPopup, {
store,
localVue,
});
await wrapper.vm.onChangeName(value);
wrapper.vm.$data.name = value.trim();
expect(wrapper.vm.$data.name).toMatch('testValue');
expect(wrapper.vm.$data.errorMessage).toMatch('');
});
it('action on onNextClick with no name works correctly', async (): Promise<void> => {
const wrapper = mount(ApiKeysCreationPopup, {
store,
localVue,
});
wrapper.vm.$data.isLoading = false;
wrapper.vm.$data.name = '';
await wrapper.vm.onNextClick();
expect(wrapper.vm.$data.errorMessage).toMatch('API Key name can`t be empty');
});
it('action on onNextClick with name works correctly', async (): Promise<void> => {
const date = new Date(0);
const testApiKey = new ApiKey('testId', 'testName', date, 'test');
jest.spyOn(apiKeysApi, 'create').mockReturnValue(
Promise.resolve(testApiKey));
const wrapper = mount(ApiKeysCreationPopup, {
store,
localVue,
});
wrapper.vm.$data.isLoading = false;
wrapper.vm.$data.name = 'testName';
await wrapper.vm.onNextClick();
const result = await store.dispatch(CREATE, 'testName');
expect(wrapper.vm.$data.key).toBe(result.secret);
expect(wrapper.vm.$data.isLoading).toBe(false);
expect(wrapper.emitted()).toEqual({'closePopup': [[]], 'showCopyPopup': [['test']]});
});
});

View File

@ -1,19 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import ApiKeysItem from '@/components/apiKeys/ApiKeysItem.vue';
import { ApiKey } from '@/types/apiKeys';
import { mount } from '@vue/test-utils';
describe('ApiKeysItem.vue', (): void => {
it('renders correctly', (): void => {
const wrapper = mount(ApiKeysItem, {
propsData: {
itemData: new ApiKey('', '', new Date(0), ''),
},
});
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,14 +0,0 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
import NoApiKeysArea from '@/components/apiKeys/NoApiKeysArea.vue';
import { shallowMount } from '@vue/test-utils';
describe('NoApiKeysArea.vue', (): void => {
it('renders correctly', (): void => {
const wrapper = shallowMount(NoApiKeysArea);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,68 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import sinon from 'sinon';
import SortingHeader from '@/components/apiKeys/SortingHeader.vue';
import { ApiKeyOrderBy } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
import { mount } from '@vue/test-utils';
describe('SortingHeader.vue', (): void => {
it('should render correctly', (): void => {
const wrapper = mount(SortingHeader);
expect(wrapper).toMatchSnapshot();
});
it('should retrieve callback', (): void => {
const onPressSpy = sinon.spy();
const wrapper = mount(SortingHeader, {
propsData: {
onHeaderClickCallback: onPressSpy,
},
});
wrapper.find('.sort-header-container__name-item').trigger('click');
expect(onPressSpy.callCount).toBe(1);
});
it('should change sort direction', (): void => {
const onPressSpy = sinon.spy();
const wrapper = mount(SortingHeader, {
propsData: {
onHeaderClickCallback: onPressSpy,
},
});
expect(wrapper.vm.$data.sortBy).toBe(ApiKeyOrderBy.NAME);
expect(wrapper.vm.$data.sortDirection).toBe(SortDirection.ASCENDING);
wrapper.find('.sort-header-container__name-item').trigger('click');
expect(onPressSpy.callCount).toBe(1);
expect(wrapper.vm.$data.sortBy).toBe(ApiKeyOrderBy.NAME);
expect(wrapper.vm.$data.sortDirection).toBe(SortDirection.DESCENDING);
});
it('should change sort by value', (): void => {
const onPressSpy = sinon.spy();
const wrapper = mount(SortingHeader, {
propsData: {
onHeaderClickCallback: onPressSpy,
},
});
expect(wrapper.vm.$data.sortBy).toBe(ApiKeyOrderBy.NAME);
expect(wrapper.vm.$data.sortDirection).toBe(SortDirection.ASCENDING);
wrapper.find('.sort-header-container__date-item').trigger('click');
expect(onPressSpy.callCount).toBe(1);
expect(wrapper.vm.$data.sortBy).toBe(ApiKeyOrderBy.CREATED_AT);
expect(wrapper.vm.$data.sortDirection).toBe(SortDirection.ASCENDING);
});
});

View File

@ -1,73 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ApiKeysArea renders correctly 1`] = `
<div class="api-keys-area">
<!---->
<div class="api-keys-area__container">
<apikeyscreationpopup-stub></apikeyscreationpopup-stub>
<apikeyscopypopup-stub apikeysecret=""></apikeyscopypopup-stub>
<!---->
<!---->
<!---->
<noapikeysarea-stub onbuttonclick="function () { [native code] }"></noapikeysarea-stub>
</div>
</div>
`;
exports[`ApiKeysArea renders empty screen with add key prompt 1`] = `
<div class="api-keys-area">
<!---->
<div class="api-keys-area__container">
<!---->
<!---->
<!---->
<!---->
<!---->
<div class="no-api-keys-area">
<h1 class="no-api-keys-area__title">Create Your First API Key</h1>
<p class="no-api-keys-area__sub-title">API keys give access to the project to create buckets, upload objects</p>
<div class="no-api-keys-area__button container" style="width: 180px; height: 48px;"><span class="label">Create API Key</span></div>
<div class="no-api-keys-area__steps-area">
<div class="no-api-keys-area__steps-area__numbers"><svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" class="no-api-keys-area__steps-area__numbers__icon">
<circle cx="15" cy="15" r="15" fill="#519CFF"></circle>
<path d="M17.0916 9.36364H14.7791L11.8984 11.1875V13.3693L14.5632 11.6989H14.6314V21H17.0916V9.36364Z" fill="white"></path>
</svg>
<div class="no-api-keys-area-divider"></div> <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" class="no-api-keys-area__steps-area__numbers__icon">
<circle cx="15" cy="15" r="15" fill="#519CFF"></circle>
<path d="M11.4041 21H19.6996V18.9886H14.8132V18.9091L16.5121 17.2443C18.9041 15.0625 19.5462 13.9716 19.5462 12.6477C19.5462 10.6307 17.8984 9.20455 15.4041 9.20455C12.9609 9.20455 11.2848 10.6648 11.2905 12.9489H13.6257C13.62 11.8352 14.3246 11.1534 15.3871 11.1534C16.4098 11.1534 17.1712 11.7898 17.1712 12.8125C17.1712 13.7386 16.603 14.375 15.5462 15.392L11.4041 19.2273V21Z" fill="white"></path>
</svg>
<div class="no-api-keys-area-divider"></div> <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" class="no-api-keys-area__steps-area__numbers__icon">
<circle cx="15" cy="15" r="15" fill="#519CFF"></circle>
<path d="M15.4709 21.1591C18.0845 21.1591 19.9538 19.7216 19.9482 17.733C19.9538 16.2841 19.0334 15.25 17.3232 15.0341V14.9432C18.6243 14.7102 19.522 13.7898 19.5163 12.483C19.522 10.6477 17.9141 9.20455 15.505 9.20455C13.1186 9.20455 11.3232 10.6023 11.2891 12.6136H13.647C13.6754 11.7273 14.4879 11.1534 15.4936 11.1534C16.4879 11.1534 17.1527 11.7557 17.147 12.6307C17.1527 13.5455 16.3743 14.1648 15.255 14.1648H14.1697V15.9716H15.255C16.5732 15.9716 17.397 16.6307 17.3913 17.5682C17.397 18.4943 16.6016 19.1307 15.4766 19.1307C14.3913 19.1307 13.5788 18.5625 13.5334 17.7102H11.0561C11.0959 19.7443 12.9141 21.1591 15.4709 21.1591Z" fill="white"></path>
</svg>
</div>
<div class="no-api-keys-area__steps-area__items">
<div class="no-api-keys-area__steps-area__items__create-api-key">
<h2 class="no-api-keys-area__steps-area__items__create-api-key__title">Create &amp; Save API Key</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/apiKey.jpg" alt="api key image" class="no-api-keys-area-image">
</div>
<div class="no-api-keys-area__steps-area__items__setup-uplink">
<h2 class="no-api-keys-area__steps-area__items__setup-uplink__title">Setup Uplink CLI</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/uplink.jpg" alt="setup uplink image" class="no-api-keys-area-image">
</div>
<div class="no-api-keys-area__steps-area__items__store-data">
<h2 class="no-api-keys-area__steps-area__items__store-data__title">Store Data</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/store.jpg" alt="store data image" class="no-api-keys-area-image">
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`ApiKeysArea renders empty search state correctly 1`] = `
<div class="api-keys-area">
<!---->
<div class="api-keys-area__container">
<apikeyscreationpopup-stub></apikeyscreationpopup-stub>
<apikeyscopypopup-stub apikeysecret=""></apikeyscopypopup-stub>
<!---->
<!---->
<!---->
<noapikeysarea-stub onbuttonclick="function () { [native code] }"></noapikeysarea-stub>
</div>
</div>
`;

View File

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ApiKeysCopyPopup renders correctly 1`] = `
<div class="save-api-popup">
<h2 class="save-api-popup__title">Save Your Secret API Key! It Will Appear Only Once.</h2>
<div class="save-api-popup__copy-area">
<div class="save-api-popup__copy-area__key-area">
<p class="save-api-popup__copy-area__key-area__key">test</p>
</div>
<p class="save-api-popup__copy-area__copy-button">Copy</p>
</div>
<div class="save-api-popup__next-step-area"><span class="save-api-popup__next-step-area__label">Next Step:</span> <a href="https://documentation.tardigrade.io/getting-started/uploading-your-first-object/set-up-uplink-cli" target="_blank" rel="noopener noreferrer" class="save-api-popup__next-step-area__link">
Set Up Uplink CLI
</a>
<div class="container" style="width: 156px; height: 40px;"><span class="label">Done</span></div>
</div>
<div class="blur-content"></div>
</div>
`;

View File

@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ApiKeysCreationPopup renders correctly 1`] = `
<div class="new-api-key">
<h2 class="new-api-key__title">Name Your API Key</h2>
<div class="input-wrap full-input">
<div class="label-container">
<!---->
<!---->
<!---->
</div> <input placeholder="Enter API Key Name" type="text" class="headerless-input" style="width: 100%; height: 48px;">
<!---->
<!---->
</div>
<div class="next-button container" style="width: 128px; height: 48px;"><span class="label">Next &gt;</span></div>
<div class="new-api-key__close-cross-container"><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7071 1.70711C16.0976 1.31658 16.0976 0.683417 15.7071 0.292893C15.3166 -0.0976311 14.6834 -0.0976311 14.2929 0.292893L15.7071 1.70711ZM0.292893 14.2929C-0.0976311 14.6834 -0.0976311 15.3166 0.292893 15.7071C0.683417 16.0976 1.31658 16.0976 1.70711 15.7071L0.292893 14.2929ZM1.70711 0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417 -0.0976311 1.31658 0.292893 1.70711L1.70711 0.292893ZM14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L14.2929 15.7071ZM14.2929 0.292893L0.292893 14.2929L1.70711 15.7071L15.7071 1.70711L14.2929 0.292893ZM0.292893 1.70711L14.2929 15.7071L15.7071 14.2929L1.70711 0.292893L0.292893 1.70711Z" fill="#384B65" class="close-cross-svg-path"></path>
</svg></div>
<div class="blur-content"></div>
</div>
`;

View File

@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ApiKeysItem.vue renders correctly 1`] = `
<div class="apikey-item-container">
<div class="apikey-item-container__common-info">
<div class="checkbox-container"><svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg" class="checkbox-container__image">
<rect x="0.75" y="0.75" width="21.5" height="21.5" rx="3.25" stroke="#384B65" stroke-opacity="0.4" stroke-width="1.5" class="checkbox-container__image__rect"></rect>
</svg></div>
<div class="avatar"><svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" class="avatar__image">
<path d="M0 6C0 2.68629 2.68629 0 6 0H24C27.3137 0 30 2.68629 30 6V24C30 27.3137 27.3137 30 24 30H6C2.68629 30 0 27.3137 0 24V6Z" fill="#2683FF"></path>
<path d="M11.9184 11C13.4286 11 14.7239 11.8848 15.3777 13.1667H22.0408C22.1155 13.1667 22.1875 13.1921 22.2449 13.2396L23.8776 14.5886C23.9573 14.6537 24.0026 14.7533 23.9994 14.8581C23.9968 14.9623 23.9458 15.0593 23.8623 15.1198L21.5766 16.7708C21.456 16.8581 21.2928 16.8516 21.1786 16.7552L20.4082 16.099L19.6378 16.7552C19.5172 16.8581 19.3406 16.8581 19.2194 16.7552L18.449 16.099L17.6786 16.7552C17.6205 16.8053 17.5465 16.8327 17.4694 16.8333H15.3724C14.7174 18.112 13.426 19 11.9184 19C9.75837 19 8 17.205 8 15C8 12.795 9.75837 11 11.9184 11H11.9184Z" fill="white"></path>
<path d="M10.7754 13.666C11.4928 13.666 12.0815 14.2669 12.0815 14.9993C12.0815 15.7318 11.4928 16.3327 10.7754 16.3327C10.0579 16.3327 9.46924 15.7318 9.46924 14.9993C9.46924 14.2669 10.0579 13.666 10.7754 13.666Z" fill="#2683FF"></path>
<path d="M10.7756 14.334C10.4108 14.334 10.1226 14.6283 10.1226 15.0007C10.1226 15.3731 10.4108 15.6673 10.7756 15.6673C11.1404 15.6673 11.4287 15.3731 11.4287 15.0007C11.4287 14.6283 11.1404 14.334 10.7756 14.334Z" fill="#2683FF"></path>
</svg></div>
<div title="" class="name-container">
<p class="name"></p>
</div>
</div>
<div class="apikey-item-container__common-info date-item-container">
<p class="date">1/1/1970</p>
</div>
</div>
`;

View File

@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NoApiKeysArea.vue renders correctly 1`] = `
<div class="no-api-keys-area">
<h1 class="no-api-keys-area__title">Create Your First API Key</h1>
<p class="no-api-keys-area__sub-title">API keys give access to the project to create buckets, upload objects</p>
<vbutton-stub label="Create API Key" width="180px" height="48px" class="no-api-keys-area__button"></vbutton-stub>
<div class="no-api-keys-area__steps-area">
<div class="no-api-keys-area__steps-area__numbers">
<firststepicon-stub class="no-api-keys-area__steps-area__numbers__icon"></firststepicon-stub>
<div class="no-api-keys-area-divider"></div>
<secondstepicon-stub class="no-api-keys-area__steps-area__numbers__icon"></secondstepicon-stub>
<div class="no-api-keys-area-divider"></div>
<thirdstepicon-stub class="no-api-keys-area__steps-area__numbers__icon"></thirdstepicon-stub>
</div>
<div class="no-api-keys-area__steps-area__items">
<div class="no-api-keys-area__steps-area__items__create-api-key">
<h2 class="no-api-keys-area__steps-area__items__create-api-key__title">Create &amp; Save API Key</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/apiKey.jpg" alt="api key image" class="no-api-keys-area-image">
</div>
<div class="no-api-keys-area__steps-area__items__setup-uplink">
<h2 class="no-api-keys-area__steps-area__items__setup-uplink__title">Setup Uplink CLI</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/uplink.jpg" alt="setup uplink image" class="no-api-keys-area-image">
</div>
<div class="no-api-keys-area__steps-area__items__store-data">
<h2 class="no-api-keys-area__steps-area__items__store-data__title">Store Data</h2> <img src="@/../static/images/apiKeys/noApiKeysArea/store.jpg" alt="store data image" class="no-api-keys-area-image">
</div>
</div>
</div>
</div>
`;

View File

@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SortingHeader.vue should render correctly 1`] = `
<div class="sort-header-container">
<div class="sort-header-container__name-item">
<p class="sort-header-container__name-item__title">Key Name</p>
<div class="container"><svg width="9" height="6" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" class="">
<path d="M4.73684 5.70565e-07L9 6L-9.53674e-07 6L4.73684 5.70565e-07Z" fill="#354049" class="arrow-svg-path"></path>
</svg> <svg width="9" height="6" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" class="active">
<path d="M4.26316 6L1.90735e-06 0L9 1.59559e-06L4.26316 6Z" fill="#354049" class="arrow-svg-path"></path>
</svg></div>
</div>
<div class="sort-header-container__date-item">
<p class="sort-header-container__date-item__title creation-date">Created</p>
<div class="container"><svg width="9" height="6" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" class="">
<path d="M4.73684 5.70565e-07L9 6L-9.53674e-07 6L4.73684 5.70565e-07Z" fill="#354049" class="arrow-svg-path"></path>
</svg> <svg width="9" height="6" viewBox="0 0 9 6" fill="none" xmlns="http://www.w3.org/2000/svg" class="">
<path d="M4.26316 6L1.90735e-06 0L9 1.59559e-06L4.26316 6Z" fill="#354049" class="arrow-svg-path"></path>
</svg></div>
</div>
</div>
`;

View File

@ -9,6 +9,8 @@ exports[`HeaderlessInput.vue renders correctly with default props 1`] = `
</div> <input placeholder="default" type="text" class="headerless-input" style="width: 100%; height: 48px;">
<!---->
<!---->
<!---->
<!---->
</div>
`;
@ -18,7 +20,9 @@ exports[`HeaderlessInput.vue renders correctly with isPassword prop 1`] = `
<!---->
<!---->
<!---->
</div> <input placeholder="default" type="password" class="headerless-input password" style="width: 100%; height: 48px;"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="input-wrap__image">
</div> <input placeholder="default" type="password" class="headerless-input password" style="width: 100%; height: 48px;">
<!---->
<!----> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="input-wrap__image">
<path d="M10 4C4.70642 4 1 10 1 10C1 10 3.6999 16 10 16C16.3527 16 19 10 19 10C19 10 15.3472 4 10 4ZM10 13.8176C7.93537 13.8176 6.2946 12.1271 6.2946 10C6.2946 7.87285 7.93537 6.18239 10 6.18239C12.0646 6.18239 13.7054 7.87285 13.7054 10C13.7054 12.1271 12.0646 13.8176 10 13.8176Z" fill="#AFB7C1" class="input-wrap__image__path"></path>
<path d="M11.6116 9.96328C11.6116 10.8473 10.8956 11.5633 10.0116 11.5633C9.12763 11.5633 8.41162 10.8473 8.41162 9.96328C8.41162 9.07929 9.12763 8.36328 10.0116 8.36328C10.8956 8.36328 11.6116 9.07929 11.6116 9.96328Z" fill="#AFB7C1"></path>
</svg>
@ -35,5 +39,7 @@ exports[`HeaderlessInput.vue renders correctly with size props 1`] = `
</div> <input placeholder="test" type="text" class="headerless-input" style="width: 30px; height: 20px;">
<!---->
<!---->
<!---->
<!---->
</div>
`;

View File

@ -1,28 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { ApiKey, ApiKeyCursor, ApiKeysApi, ApiKeysPage } from '@/types/apiKeys';
/**
* Mock for ApiKeysApi
*/
export class ApiKeysMock implements ApiKeysApi {
private readonly date = new Date(0);
private mockApiKeysPage: ApiKeysPage;
public setMockApiKeysPage(mockApiKeysPage: ApiKeysPage): void {
this.mockApiKeysPage = mockApiKeysPage;
}
get(projectId: string, cursor: ApiKeyCursor): Promise<ApiKeysPage> {
return Promise.resolve(this.mockApiKeysPage);
}
create(projectId: string, name: string): Promise<ApiKey> {
return Promise.resolve(new ApiKey('testId', 'testName', this.date, 'testKey'));
}
delete(ids: string[]): Promise<void> {
return Promise.resolve();
}
}

View File

@ -1,326 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import Vuex from 'vuex';
import { ApiKeysApiGql } from '@/api/apiKeys';
import { ProjectsApiGql } from '@/api/projects';
import { API_KEYS_ACTIONS, API_KEYS_MUTATIONS, makeApiKeysModule } from '@/store/modules/apiKeys';
import { makeProjectsModule } from '@/store/modules/projects';
import { ApiKey, ApiKeyOrderBy, ApiKeysPage } from '@/types/apiKeys';
import { SortDirection } from '@/types/common';
import { Project } from '@/types/projects';
import { createLocalVue } from '@vue/test-utils';
const Vue = createLocalVue();
const apiKeysApi = new ApiKeysApiGql();
const apiKeysModule = makeApiKeysModule(apiKeysApi);
const {
FETCH,
CREATE,
CLEAR_SELECTION,
DELETE,
CLEAR,
} = API_KEYS_ACTIONS;
const projectsApi = new ProjectsApiGql();
const projectsModule = makeProjectsModule(projectsApi);
const selectedProject = new Project('', '', '', '');
selectedProject.id = '1';
projectsModule.state.selectedProject = selectedProject;
const date = new Date(0);
const apiKey = new ApiKey('testId', 'testName', date, 'testSecret');
const apiKey2 = new ApiKey('testId2', 'testName2', date, 'testSecret2');
const FIRST_PAGE = 1;
const TEST_ERROR = 'testError';
const UNREACHABLE_ERROR = 'should be unreachable';
Vue.use(Vuex);
const store = new Vuex.Store({modules: {projectsModule, apiKeysModule}});
const state = (store.state as any).apiKeysModule;
describe('mutations', (): void => {
it('fetch api keys', (): void => {
const testApiKeysPage = new ApiKeysPage();
testApiKeysPage.apiKeys = [apiKey];
testApiKeysPage.totalCount = 1;
testApiKeysPage.pageCount = 1;
store.commit(API_KEYS_MUTATIONS.SET_PAGE, testApiKeysPage);
expect(state.page.apiKeys.length).toBe(1);
expect(state.page.search).toBe('');
expect(state.page.order).toBe(ApiKeyOrderBy.NAME);
expect(state.page.orderDirection).toBe(SortDirection.ASCENDING);
expect(state.page.limit).toBe(6);
expect(state.page.pageCount).toBe(1);
expect(state.page.currentPage).toBe(1);
expect(state.page.totalCount).toBe(1);
});
it('set api keys page', (): void => {
store.commit(API_KEYS_MUTATIONS.SET_PAGE_NUMBER, 2);
expect(state.cursor.page).toBe(2);
});
it('set search query', (): void => {
store.commit(API_KEYS_MUTATIONS.SET_SEARCH_QUERY, 'testSearchQuery');
expect(state.cursor.search).toBe('testSearchQuery');
});
it('set sort order', (): void => {
store.commit(API_KEYS_MUTATIONS.CHANGE_SORT_ORDER, ApiKeyOrderBy.CREATED_AT);
expect(state.cursor.order).toBe(ApiKeyOrderBy.CREATED_AT);
});
it('set sort direction', (): void => {
store.commit(API_KEYS_MUTATIONS.CHANGE_SORT_ORDER_DIRECTION, SortDirection.DESCENDING);
expect(state.cursor.orderDirection).toBe(SortDirection.DESCENDING);
});
it('toggle selection', (): void => {
store.commit(API_KEYS_MUTATIONS.TOGGLE_SELECTION, apiKey);
expect(state.page.apiKeys[0].isSelected).toBe(true);
expect(state.selectedApiKeysIds.length).toBe(1);
store.commit(API_KEYS_MUTATIONS.TOGGLE_SELECTION, apiKey);
expect(state.page.apiKeys[0].isSelected).toBe(false);
expect(state.selectedApiKeysIds.length).toBe(0);
});
it('clear selection', (): void => {
store.commit(API_KEYS_MUTATIONS.CLEAR_SELECTION);
state.page.apiKeys.forEach((key: ApiKey) => {
expect(key.isSelected).toBe(false);
});
expect(state.selectedApiKeysIds.length).toBe(0);
});
it('clear store', (): void => {
store.commit(API_KEYS_MUTATIONS.CLEAR);
expect(state.cursor.page).toBe(1);
expect(state.cursor.search).toBe('');
expect(state.cursor.order).toBe(ApiKeyOrderBy.NAME);
expect(state.cursor.orderDirection).toBe(SortDirection.ASCENDING);
expect(state.page.apiKeys.length).toBe(0);
expect(state.selectedApiKeysIds.length).toBe(0);
});
});
describe('actions', (): void => {
beforeEach((): void => {
jest.resetAllMocks();
});
it('success fetch apiKeys', async (): Promise<void> => {
const testApiKeysPage = new ApiKeysPage();
testApiKeysPage.apiKeys = [apiKey];
testApiKeysPage.totalCount = 1;
testApiKeysPage.pageCount = 1;
jest.spyOn(apiKeysApi, 'get').mockReturnValue(
Promise.resolve(testApiKeysPage),
);
await store.dispatch(FETCH, FIRST_PAGE);
expect(state.page.apiKeys[0].id).toBe(apiKey.id);
expect(state.page.apiKeys[0].name).toBe(apiKey.name);
expect(state.page.apiKeys[0].createdAt).toBe(apiKey.createdAt);
expect(state.page.apiKeys[0].secret).toBe(apiKey.secret);
});
it('fetch throws an error when api call fails', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'get').mockImplementation(() => {
throw new Error(TEST_ERROR);
});
try {
await store.dispatch(FETCH);
} catch (error) {
store.commit(API_KEYS_MUTATIONS.CHANGE_SORT_ORDER_DIRECTION, SortDirection.DESCENDING);
expect(error.message).toBe(TEST_ERROR);
return;
}
fail(UNREACHABLE_ERROR);
});
it('success create apiKeys', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'create').mockReturnValue(Promise.resolve(apiKey));
try {
await store.dispatch(CREATE, 'testName');
throw new Error(TEST_ERROR);
} catch (error) {
expect(error.message).toBe(TEST_ERROR);
}
});
it('create throws an error when api call fails', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'create').mockImplementation(() => {
throw new Error(TEST_ERROR);
});
try {
await store.dispatch(CREATE, 'testName');
} catch (error) {
expect(error.message).toBe(TEST_ERROR);
return;
}
fail(UNREACHABLE_ERROR);
});
it('success delete apiKeys', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'delete').mockReturnValue(
Promise.resolve(),
);
try {
await store.dispatch(DELETE, ['testId', 'testId']);
throw new Error(TEST_ERROR);
} catch (error) {
expect(error.message).toBe(TEST_ERROR);
}
});
it('delete throws an error when api call fails', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'delete').mockImplementation(() => {
throw new Error(TEST_ERROR);
});
try {
await store.dispatch(DELETE, 'testId');
} catch (error) {
expect(error.message).toBe(TEST_ERROR);
return;
}
fail(UNREACHABLE_ERROR);
});
it('set api keys search query', async (): Promise<void> => {
await store.dispatch(API_KEYS_ACTIONS.SET_SEARCH_QUERY, 'search');
expect(state.cursor.search).toBe('search');
});
it('set api keys sort by', async (): Promise<void> => {
await store.dispatch(API_KEYS_ACTIONS.SET_SORT_BY, ApiKeyOrderBy.CREATED_AT);
expect(state.cursor.order).toBe(ApiKeyOrderBy.CREATED_AT);
});
it('set sort direction', async (): Promise<void> => {
await store.dispatch(API_KEYS_ACTIONS.SET_SORT_DIRECTION, SortDirection.DESCENDING);
expect(state.cursor.orderDirection).toBe(SortDirection.DESCENDING);
});
it('success toggleAPIKeySelection apiKeys', async (): Promise<void> => {
jest.spyOn(apiKeysApi, 'get').mockReturnValue(
Promise.resolve(new ApiKeysPage([apiKey, apiKey2],
'',
ApiKeyOrderBy.NAME,
SortDirection.ASCENDING,
6,
2,
1,
2,
)),
);
await store.dispatch(API_KEYS_ACTIONS.FETCH, FIRST_PAGE);
await store.dispatch(API_KEYS_ACTIONS.TOGGLE_SELECTION, apiKey);
expect(state.page.apiKeys[0].isSelected).toBe(true);
expect(state.selectedApiKeysIds.length).toBe(1);
await store.dispatch(API_KEYS_ACTIONS.TOGGLE_SELECTION, apiKey2);
expect(state.page.apiKeys[1].isSelected).toBe(true);
expect(state.selectedApiKeysIds.length).toBe(2);
await store.dispatch(API_KEYS_ACTIONS.FETCH, FIRST_PAGE);
expect(state.page.apiKeys[1].isSelected).toBe(true);
expect(state.selectedApiKeysIds.length).toBe(2);
await store.dispatch(API_KEYS_ACTIONS.TOGGLE_SELECTION, apiKey2);
expect(state.page.apiKeys[1].isSelected).toBe(false);
expect(state.selectedApiKeysIds.length).toBe(1);
});
it('success clearSelection apiKeys', async (): Promise<void> => {
await store.dispatch(CLEAR_SELECTION);
state.page.apiKeys.forEach((key: ApiKey) => {
expect(key.isSelected).toBe(false);
});
});
it('success clearAPIKeys', async (): Promise<void> => {
await store.dispatch(CLEAR);
expect(state.cursor.search).toBe('');
expect(state.cursor.limit).toBe(6);
expect(state.cursor.page).toBe(1);
expect(state.cursor.order).toBe(ApiKeyOrderBy.NAME);
expect(state.cursor.orderDirection).toBe(SortDirection.ASCENDING);
expect(state.page.apiKeys.length).toBe(0);
expect(state.page.search).toBe('');
expect(state.page.order).toBe(ApiKeyOrderBy.NAME);
expect(state.page.orderDirection).toBe(SortDirection.ASCENDING);
expect(state.page.limit).toBe(6);
expect(state.page.pageCount).toBe(0);
expect(state.page.currentPage).toBe(1);
expect(state.page.totalCount).toBe(0);
state.page.apiKeys.forEach((key: ApiKey) => {
expect(key.isSelected).toBe(false);
});
});
});
describe('getters', (): void => {
const selectedApiKey = new ApiKey('testtestId', 'testtestName', date, 'testtestSecret');
it('selected apiKeys', (): void => {
const testApiKeysPage = new ApiKeysPage();
testApiKeysPage.apiKeys = [selectedApiKey];
testApiKeysPage.totalCount = 1;
testApiKeysPage.pageCount = 1;
store.commit(API_KEYS_MUTATIONS.SET_PAGE, testApiKeysPage);
store.commit(API_KEYS_MUTATIONS.TOGGLE_SELECTION, selectedApiKey);
const retrievedApiKeys = store.getters.selectedApiKeys;
expect(retrievedApiKeys[0].id).toBe('testtestId');
});
it('apiKeys array', (): void => {
const retrievedApiKeys = store.getters.selectedApiKeys;
expect(retrievedApiKeys).toEqual([selectedApiKey]);
});
});

View File

@ -21,7 +21,6 @@ import DashboardArea from '@/views/DashboardArea.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { AccessGrantsMock } from '../mock/api/accessGrants';
import { ApiKeysMock } from '../mock/api/apiKeys';
import { BucketsMock } from '../mock/api/buckets';
import { PaymentsMock } from '../mock/api/payments';
import { ProjectMembersApiMock } from '../mock/api/projectMembers';

View File

@ -2,6 +2,7 @@
exports[`Dashboard renders correctly when data is loaded 1`] = `
<div class="dashboard">
<!---->
<!---->
<!---->
<div class="dashboard__wrap">
@ -25,5 +26,6 @@ exports[`Dashboard renders correctly when data is loading 1`] = `
<div class="loading-overlay active"><img src="@/../static/images/register/Loading.gif" alt="Company logo loading gif" class="loading-image"></div>
<!---->
<!---->
<!---->
</div>
`;

View File

@ -164,12 +164,12 @@ export default class SNOHeader extends Vue {
public async onRefresh(): Promise<void> {
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, true);
const selectedSatellite = this.$store.state.node.selectedSatellite.id;
const selectedSatelliteId = this.$store.state.node.selectedSatellite.id;
await this.$store.dispatch(APPSTATE_ACTIONS.SET_NO_PAYOUT_DATA, false);
try {
await this.$store.dispatch(GET_NODE_INFO);
await this.$store.dispatch(SELECT_SATELLITE, selectedSatellite);
await this.$store.dispatch(SELECT_SATELLITE, selectedSatelliteId);
} catch (error) {
console.error(`${error.message} satellite data.`);
}
@ -189,7 +189,7 @@ export default class SNOHeader extends Vue {
await this.$store.dispatch(APPSTATE_ACTIONS.SET_LOADING, false);
try {
await this.$store.dispatch(PAYOUT_ACTIONS.GET_PAYOUT_INFO, selectedSatellite);
await this.$store.dispatch(PAYOUT_ACTIONS.GET_PAYOUT_INFO, selectedSatelliteId);
await this.$store.dispatch(PAYOUT_ACTIONS.GET_TOTAL);
} catch (error) {
console.error(error.message);

View File

@ -107,6 +107,21 @@
<p class="estimation-table-container__net-total-area__text">{{ totalPayout | centsToDollars }}</p>
</div>
</div>
<div class="estimation-table-container__distributed-area" v-if="!isCurrentPeriod && !isLastPeriodWithoutPaystub">
<div class="estimation-table-container__distributed-area__left-area">
<p class="estimation-table-container__distributed-area__text">Distributed</p>
<div class="estimation-table-container__distributed-area__info-area">
<ChecksInfoIcon class="checks-area-image" alt="Info icon with question mark" @mouseenter="toggleTooltipVisibility" @mouseleave="toggleTooltipVisibility"/>
<div class="tooltip" v-show="isTooltipVisible">
<div class="tooltip__text-area">
<p class="tooltip__text-area__text">If you see $0.00 as your distributed amount, you didnt reach the minimum payout threshold. Your payout will be distributed along with one of the payouts in the upcoming payout cycles. If you see a distributed amount higher than expected, it means this month you were paid undistributed payouts from previous months in addition to this months payout.</p>
</div>
<div class="tooltip__footer"></div>
</div>
</div>
</div>
<p class="estimation-table-container__distributed-area__text">{{ totalPaystubForPeriod.distributed | centsToDollars }}</p>
</div>
</div>
<div class="estimation-container__payout-area" v-if="isCurrentPeriod && !isFirstDayOfCurrentMonth">
<div class="estimation-container__payout-area__left-area">
@ -130,6 +145,8 @@ import { Component, Vue } from 'vue-property-decorator';
import EstimationPeriodDropdown from '@/app/components/payments/EstimationPeriodDropdown.vue';
import ChecksInfoIcon from '@/../static/images/checksInfo.svg';
import { APPSTATE_ACTIONS } from '@/app/store/modules/appState';
import {
BANDWIDTH_DOWNLOAD_PRICE_PER_TB,
@ -161,6 +178,7 @@ class EstimationTableRow {
@Component ({
components: {
EstimationPeriodDropdown,
ChecksInfoIcon,
},
})
export default class EstimationArea extends Vue {
@ -386,6 +404,18 @@ export default class EstimationArea extends Vue {
return this.now.getUTCDate() === 1;
}
/**
* Indicates if tooltip needs to be shown.
*/
public isTooltipVisible: boolean = false;
/**
* Toggles tooltip visibility.
*/
public toggleTooltipVisibility(): void {
this.isTooltipVisible = !this.isTooltipVisible;
}
/**
* Selects current month as selected payout period.
*/
@ -580,7 +610,8 @@ export default class EstimationArea extends Vue {
}
&__net-total-area,
&__total-area {
&__total-area,
&__distributed-area {
display: flex;
align-items: center;
justify-content: center;
@ -595,10 +626,15 @@ export default class EstimationArea extends Vue {
}
}
&__net-total-area {
&__net-total-area,
&__distributed-area {
background-color: var(--estimation-table-total-container-color);
}
&__net-total-area {
border-bottom: 1px solid #a9b5c1;
}
&__total-area {
align-items: center;
justify-content: space-between;
@ -608,6 +644,24 @@ export default class EstimationArea extends Vue {
font-family: 'font_regular', sans-serif;
}
}
&__distributed-area {
justify-content: space-between;
font-family: 'font_regular', sans-serif;
&__info-area {
position: relative;
margin-left: 10px;
width: 18px;
height: 18px;
}
&__left-area {
display: flex;
align-items: center;
justify-content: center;
}
}
}
.short-text {
@ -679,6 +733,38 @@ export default class EstimationArea extends Vue {
}
}
.tooltip {
position: absolute;
bottom: 35px;
left: 50%;
transform: translate(-50%);
height: auto;
box-shadow: 0 2px 48px var(--tooltip-shadow-color);
border-radius: 12px;
background: var(--tooltip-background-color);
&__text-area {
padding: 15px 11px;
width: 360px;
font-family: 'font_regular', sans-serif;
font-size: 11px;
line-height: 17px;
color: var(--regular-text-color);
text-align: center;
}
&__footer {
position: absolute;
left: 50%;
transform: translate(-50%);
width: 0;
height: 0;
border-style: solid;
border-width: 11.5px 11.5px 0 11.5px;
border-color: var(--tooltip-background-color) transparent transparent transparent;
}
}
@media screen and (max-width: 870px) {
.estimation-container {

View File

@ -13,7 +13,7 @@
<div class="payout-history-table__table-container">
<div class="payout-history-table__table-container__labels-area">
<p class="payout-history-table__table-container__labels-area__label">Satellite</p>
<p class="payout-history-table__table-container__labels-area__label">Paid</p>
<p class="payout-history-table__table-container__labels-area__label">Payout</p>
</div>
<PayoutHistoryTableItem v-for="historyItem in payoutHistory" :key="historyItem.satelliteID" :history-item="historyItem" />
<div class="payout-history-table__table-container__totals-area">

View File

@ -18,7 +18,7 @@
<div class="payout-history-item__expanded-area__left-area__info-area">
<div class="payout-history-item__expanded-area__left-area__info-area__item flex-start">
<p class="payout-history-item__expanded-area__left-area__info-area__item__label extra-margin">Node Age</p>
<p class="payout-history-item__expanded-area__left-area__info-area__item__value">{{ historyItem.age }} Month</p>
<p class="payout-history-item__expanded-area__left-area__info-area__item__value">{{ `${historyItem.age} Month${historyItem.age > 1 ? 's' : ''}` }}</p>
</div>
<div class="payout-history-item__expanded-area__left-area__info-area__item flex-start">
<p class="payout-history-item__expanded-area__left-area__info-area__item__label extra-margin">Earned</p>
@ -78,6 +78,10 @@
<p class="payout-history-item__expanded-area__right-area__info-item__label">Held Returned</p>
<p class="payout-history-item__expanded-area__right-area__info-item__value">{{ historyItem.disposed | centsToDollars }}</p>
</div>
<div class="payout-history-item__expanded-area__right-area__info-item">
<p class="payout-history-item__expanded-area__right-area__info-item__label">Distributed</p>
<p class="payout-history-item__expanded-area__right-area__info-item__value">{{ historyItem.distributed | centsToDollars }}</p>
</div>
<div class="payout-history-item__expanded-area__right-area__divider"></div>
<div class="payout-history-item__expanded-area__right-area__footer">
<div class="payout-history-item__expanded-area__right-area__footer__transaction" v-if="historyItem.transactionLink">

View File

@ -14,8 +14,8 @@
<p class="payout-area-container__section-title">Balance</p>
<section class="payout-area-container__balance-area">
<div class="row">
<SingleInfo width="48%" label="Node Operator Balance" :value="balance | centsToDollars" info-text="You need to earn the minimum withdrawal amount so that we can transfer the entire amount to the wallet at the end of the month, otherwise it will remain on your balance for the next month or until you accumulate the minimum withdrawal amount" />
<SingleInfo width="48%" :label="`Clean earnings, ${currentPeriod}`" :value="currentMonthExpectations | centsToDollars" info-text="Estimated payout at the end of the month. This is only an estimate and may nor reflect actual payout amount." />
<SingleInfo width="48%" label="Undistributed payout" :value="balance | centsToDollars" info-text="You need to earn the minimum withdrawal amount so that we can transfer the entire amount to the wallet at the end of the month, otherwise it will remain on your balance for the next month or until you accumulate the minimum withdrawal amount" />
<SingleInfo width="48%" label="Estimated earning this month" :value="currentMonthExpectations | centsToDollars" info-text="Estimated payout at the end of the month. This is only an estimate and may not reflect actual payout amount." />
</div>
</section>
<p class="payout-area-container__section-title">Payout</p>
@ -142,18 +142,6 @@ export default class PayoutArea extends Vue {
return this.$store.state.payoutModule.payoutPeriods;
}
/**
* Returns formatted selected payout period.
*/
public get currentPeriod(): string {
const start: PayoutPeriod = this.$store.state.payoutModule.periodRange.start;
const end: PayoutPeriod = this.$store.state.payoutModule.periodRange.end;
return start && start.period !== end.period ?
`${monthNames[start.month].slice(0, 3)} ${start.year} - ${monthNames[end.month].slice(0, 3)} ${end.year}`
: `${monthNames[end.month].slice(0, 3)} ${end.year}`;
}
public get currentMonthExpectations(): number {
return this.$store.state.payoutModule.estimation.currentMonthExpectations;
}

View File

@ -46,7 +46,8 @@ export class PayoutHttpApi implements PayoutApi {
throw new Error('can not get held information');
}
const data: any[] = await response.json() || [];
const responseBody = await response.json() || [];
const data: any[] = !Array.isArray(responseBody) ? [ responseBody ] : responseBody;
return data.map((paystubJson: any) => {
return new Paystub(
@ -130,6 +131,7 @@ export class PayoutHttpApi implements PayoutApi {
payoutHistoryItem.receipt,
payoutHistoryItem.isExitComplete,
payoutHistoryItem.heldPercent,
payoutHistoryItem.distributed,
);
});
}

View File

@ -137,6 +137,7 @@ export class TotalPaystubForPeriod {
public paid: number = 0;
public paidWithoutSurge: number = 0;
public grossWithSurge: number = 0;
public distributed: number = 0;
public constructor(
paystubs: Paystub[] = [],
@ -161,6 +162,7 @@ export class TotalPaystubForPeriod {
this.surgePercent = paystub.surgePercent;
this.paidWithoutSurge += this.convertToCents(paystub.paid + paystub.held - paystub.disposed) / paystub.surgeMultiplier;
this.grossWithSurge += this.convertToCents(paystub.paid + paystub.held - paystub.disposed);
this.distributed += this.convertToCents(paystub.distributed);
});
}
@ -276,6 +278,7 @@ export class SatellitePayoutForPeriod {
public receipt: string = '',
public isExitComplete: boolean = false,
public heldPercent: number = 0,
public distributed: number = 0,
) {
this.earned = this.convertToCents(this.earned);
this.surge = this.convertToCents(this.surge);
@ -283,6 +286,7 @@ export class SatellitePayoutForPeriod {
this.afterHeld = this.convertToCents(this.afterHeld);
this.disposed = this.convertToCents(this.disposed);
this.paid = this.convertToCents(this.paid);
this.distributed = this.convertToCents(this.distributed);
}
public get transactionLink(): string {

View File

@ -169,6 +169,7 @@ describe('EstimationArea', (): void => {
paystub.held = 777777;
paystub.paid = 555555;
paystub.surgePercent = 300;
paystub.distributed = 333333;
const totalPaystubForPeriod = new TotalPaystubForPeriod([paystub]);
await store.commit(PAYOUT_MUTATIONS.SET_PERIODS, [payoutPeriod]);

View File

@ -28,10 +28,10 @@ describe('PayoutHistoryTable', (): void => {
it('renders correctly with actual values', async (): Promise<void> => {
await store.commit(PAYOUT_MUTATIONS.SET_PAYOUT_HISTORY, [
new SatellitePayoutForPeriod('1', 'name1', 1, 100000, 1200000, 140,
500000, 600000, 200000, 800000, 'receipt1', false,
500000, 600000, 200000, 800000, 'receipt1', false, 400000,
),
new SatellitePayoutForPeriod('2', 'name2', 16, 100000, 1200000, 140,
500000, 600000, 200000, 800000, 'receipt2', true,
500000, 600000, 200000, 800000, 'receipt2', true, 400000,
),
]);

Some files were not shown because too many files have changed in this diff Show More