diff --git a/CHANGELOG/CHANGELOG-5.0.0-beta5.md b/CHANGELOG/CHANGELOG-5.0.0-beta5.md new file mode 100644 index 00000000000..ebcec98457a --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.0-beta5.md @@ -0,0 +1,157 @@ +Welcome to the v5.0.0-beta5 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 8f135d6d708fb6e097f979ff98149b100cc779ef: feat(desktop): add appToken for providers & fix ssr error (#4566) (@xudaotutou) +* b766fb5abeb3c2837140088dfac90778c076af70: feat(desktop): multi region (#4558) (@xudaotutou) +* 559154f5370c41495ea94a9590b5b91e407e3fca: feat(frontend): static host (#4634) (@xudaotutou) +* 0180fad6c713439c96655a275f033cb079436344: feat: @sealos/ui useMessage component (#4523) (@zjy365) +* 3c3563b12a3f34558dccff7cd555694832e49acc: feat: Name modification and deployment count display (#4453) (@zjy365) +* dc60d199e4b2c35b033124a24495c83ab6f97ec2: feat: launchpad api createApp with extra parameters (#4598) (@zjy365) +* 649c535284cd61ad0241411e9381cadf33175cc4: feat: object storage cluster init. (#4510) (@lingdie) +* f3de924d870cef4b81d6248e4a68aba4fcb2a4fc: feat: support multi domains for ingress webhooks. (#4491) (@lingdie) +* 3bdcc735af3d2c5106550443a57058ca82d49e11: feat:Desktop add standard OAuth2 login support (#4671) (@zjy365) +* 2719247f315e212817300dc6f3253fd7f27b0307: feat:Improve App Store rendering speed (#4609) (@zjy365) +* dfb748f9e8aff53109d3ce76d5689b1895d57cc7: feat:app logo colorful & blue bg (#4488) (@zjy365) +* 9125932cd491cdebbb579371bfe9a1439ec55602: feat:db adapt mongo instance count (#4490) (@zjy365) +* aec7020f77a2ecaf677a48a30db9fe13f1ea2575: feat:db provider adapted to 0.8.1 (#4508) (@zjy365) +* 046bb27270afeaa5536141516757f4876e6ef26d: feat:desktop support system configuration (#4538) (@zjy365) +* 84e033d06941d1dac64570fecb6620209e3dbc03: feat:desktop support wechat public account login (#4519) (@zjy365) +* 7155582f1d8d277cad9962b9c3fd52c831e6bfd0: feat:desktop supports invitations (#4613) (@zjy365) +* 63a693af6aa7296f72770d7a926f9429ade10118: feat:docs sale banner (#4564) (@zjy365) +* 08a9f55ddbf6774cbc9571e57f3eec9c57c66510: feat:docs self-hosting entrance (#4521) (@zjy365) +* 929faae785d9e5ceb6be843c313106e4b15f4117: feat:invite app add deployment files (#4614) (@zjy365) +* ae7f63f9b2f848c55bb815ad5b4fb6bf24776230: feat:launchpad monitor api & template ssr (#4482) (@zjy365) +* b6b8108d6e445537d2e9ba68f12a0dab62e2d01f: feat:optimize app store loading (#4607) (@zjy365) +* cd56ca8768452608e6f560c0ff4fcc9bfb73f01d: feat:template carousel image (#4485) (@zjy365) +* 16b92b814a4c9628414e67ddd5ec27ad96e2ef11: feat:template deploy add template-static (#4457) (@zjy365) +* 0495264c9c54d01330a9bd879d66645f57ff2799: feat:template language detection & instance api (#4497) (@zjy365) +* 61e69210ff8b261dc44741faf0b019fbb97b9a73: feat:template obtains service resources (#4502) (@zjy365) +* e0cc9d611982009fee2a5a19ec941c39df2a7c5c: feat:template session store (#4526) (@zjy365) +* f193f89a1129790e53b4eb1d42eb6dcf82a2bad3: feat:template support application marketplace category feature (#4583) (@zjy365) +* b2beb0a00b5b4987cd33a73f1a40ea2201ac0c0b: feat:template support base64 (#4627) (@zjy365) +* 53ecc9e6901a6e4f18b2b1b5b35a662e1d7a3ee5: feat:template support categories & fix launchpad table (#4541) (@zjy365) +* ff26b697c71ee6c663d7597300e624a54e951682: feat:template support parsing option lists (#4533) (@zjy365) +* 5492617d6f56ae6a2f325cbdd424d5dc2ed2ab34: small feat: add database tips one user select 1 replicas (#4507) (@o0vO) +* 26e06adefc8ff7486bf813dcae45b0fa43b8f482: style & feat: new UI components & automatically re-watch (#4433) (@Wishrem) +### Bug fixes +* 8fd13f63288df3bf70fd03b1fe7e0b9bf630fefe: fix(costcenter):fix rerender when select date (#4574) (@xudaotutou) +* 1eafe70326daf678bbf9fa16c32705b72419fea5: fix(desktop):add cloudflare turnstile (#4595) (@xudaotutou) +* 873894f6d2e5f3962295da2f0eb3d1cbaa36dc33: fix(desktop):fix verification code restriction (#4449) (@xudaotutou) +* a748715a85d35ecf6b3ec6419f6db731ff246d2c: fix(objectstorage):fix create bucket error (#4480) (@xudaotutou) +* 375b561a0b4ad574f787be58f8cd17b9e9239aa4: fix(objectstorage):fix predictCard (#4501) (@xudaotutou) +* a6aade62ca56b09f18d671fc2e14ffea45c9d2cb: fix: Add "Deploy" translation key to English locale (#4569) (@tianshanghong) +* 317ee5c2610f2f71738dd367f30696c116bce80a: fix: db role and select default backup repository (#4676) (@zjy365) +* 1e54b69df7f872bac945977f3f64fb3fb8d71911: fix: dbprovider resource already exists problem (#4560) (@zjy365) +* ad83b8433bb8fdb76d1483781997135d5edf7ce8: fix: desktop applaunchpad costcenter styles (#4667) (@zjy365) +* d334b54a3325e70c92035bb46c02c4b8010772ed: fix: issue #4536 (#4540) (@fengxsong) +* b93ca1eca586fc3f445723eda44958db46502c3a: fix: local page auto fall into online site. (#4513) (@o0vO) +* 20a027d9e68a140aea18e8a77a22e40d9906c15c: fix: revised version (#4604) (@rtpacks) +* 6eaec67f7fa481553cf11f17b529648c887fcf32: fix:db auth & template develop (#4445) (@zjy365) +* c35ee00d79226322a566d945fc074779e876129c: fix:db external network connection (#4615) (@zjy365) +* 6727ced5a59d4beb3baf7ba4b266f05994ab410e: fix:dbprovider auto backup (#4515) (@zjy365) +* 73b12c5cf3e6251c13b1623a472d90c4b80a681f: fix:dbprovider maximum storage (#4459) (@zjy365) +* fe50d0986028ac1e78b174c8ac0133ea5874b8a7: fix:dbprovider number of database instances (#4580) (@zjy365) +* 900d3fc8d2701ddb5560446c8442dbc3d241d482: fix:desktop keep last workspace state (#4646) (@zjy365) +* ac3aeefe6221ac6d9c40dc402054e20c340aeed7: fix:desktop modal header style & costcenter breakpoints (#4675) (@zjy365) +* 851cd69542f2bd68dc12ddb78f2f41fc19a0ccc8: fix:desktop password login and static host (#4639) (@zjy365) +* ee6b12979f6d3f65e6b3fc0f3d9a6a4400617fdd: fix:desktop region info (#4631) (@zjy365) +* 2b1ab315ecc4e2266fa0164c10c7216e1fad669a: fix:docs link address (#4666) (@zjy365) +* 8d394239d36fa3d8d6f6b6daa1d7d528dac4c20a: fix:docs reference scripts path problem (#4535) (@zjy365) +* d5c2fecfa757be8cf55a1c06bbfe63c493ef45a3: fix:docs wow.min.js (#4489) (@zjy365) +* 6c596ecf555349027c763a578bdbed12e57bba32: fix:license add CUSTOM_BASE_PATH env (#4654) (@zjy365) +* d73becbf408d9165934f6c41a14105a03c92265c: fix:privoder logo svg (#4446) (@zjy365) +* d7269a9cf9f70e9ee19479368a9104b249462b45: fix:template redirection problem (#4472) (@zjy365) +* 4ff45a89d7426b328d65489048c2a80bc67f7a5f: fix:template service nodeport (#4503) (@zjy365) +* 047428cb2dd2b69e294478a9dcd731859c311cba: fix:terminal already exists (#4651) (@zjy365) +### Other work +* c3fa80d518fd59685eaa2eed59ab2cb4dc35e9c2: :bug: fix net interface sort (#4672) (@cuisongliu) +* 7e97c72bb4460ab31c059d4673451835369bce73: Add Object Storage guide docs (#4579) (@nowinkeyy) +* 6d73bbfc3c036d84cac0ca24b52acd680c643377: Add category to template apps. (#4539) (@zzjin) +* 16c10cf3ace8b3fb37eff3940442c71773baff0a: Build kubepanel (#4551) (@lingdie) +* dabac703d3cea9eac536fa40fdd18ccbd17e6a35: Disable desktop app draggable. (#4571) (@zzjin) +* 883b7df5d9f5c1c67a0822bf7a4cc5eb6c7a76b5: Feat/e2e test (#4486) (@bxy4543) +* 4a0a90cbbe71dbe68cbd744bdb9a0e0147963eb2: Fix adminer namespace method. (#4581) (@zzjin) +* 5c73e629fd35c0b4ea3e2aa47e5ed1d2ae55e30e: Fix controller ingress label override. (#4597) (@zzjin) +* 1c960df32f4f8e41d59ce87a12413e388407cada: Fix controller memory usage, (#4589) (@zzjin) +* 8ca706929e4a078b2f7ac9f9658892c352638942: Fix dead link (#4529) (@fanux) +* 4ab847e9fcbf13813dcf97d38c3bf0103072b65f: Fix init admin account & kbcli addon enable snapshot-controller (#4643) (@bxy4543) +* 3a7a3cd072ddf93c1e7cba20ccd626183fa1884c: Fix service/launchpad typo. (#4655) (@zzjin) +* 2085ee63cf2f394b9342a642c96dbcb65dbf71d3: Fix title typo. (#4628) (@zzjin) +* af438cf5244fecc016579531f11d9728aab418be: Fix waiting for table creation (#4638) (@bxy4543) +* 14e397c38dbef4819b18cafbaeb12bf17b2a14af: Fix/traffic db name (#4548) (@bxy4543) +* 48b18e08854f2462d604e058a88c5e0adcafb2a0: Launchpad (#4645) (@wallyxjh) +* 0755c905e0bab5ebaee3096d5f34243b001c6085: License with accountv2 (#4600) (@bxy4543) +* 2337ab1150cc9867bcb67c0b71bb5036582f9403: Optimize monitor minio (#4679) (@bxy4543) +* 9318fd5a979ae8caa68bb89d4b222ca71b16ca76: Optimize the bucket controller logic of object storage (#4504) (@nowinkeyy) +* b496a20599359d469c750bbfe8822088a006cdb2: Optimize/debt resume speed (#4658) (@bxy4543) +* e819739117a410d7574746d03261b0cb22b10801: Optimize/monitor (#4481) (@bxy4543) +* a35524dfd3c7cacbc43ee92ac68cc7b35f235587: Separate database svc and minio svc (#4447) (@nowinkeyy) +* 0d9d19d4c6f0ceff1696a7c467bdfad7028fad99: The init job waits for the user table to be created before execution. (#4619) (@bxy4543) +* 5e0ea541ca1a4354f315aa9b64f7842f65f98453: Transfer service (#4594) (@bxy4543) +* 0350700b558d738e87a75724e09edc19ee429d3e: Unified account (#4576) (@bxy4543) +* a6e09647921a4e083163a245faee923c233fbd9c: Update README.md (#4492) (@fanux) +* 6b17b9503b4603bf6ef45674d8120f6b57105d3b: Update README.md (#4647) (@fanux) +* 9a9533ba5f2a08695c35e6d064f7b554ddc4043f: Update cockroach (#4605) (@wallyxjh) +* d0b30e6e66e9297f45aa0ed2afea9aaf897426dc: Update doc typo. (#4656) (@zzjin) +* 295e6a543f598b50a3dddeb935a6a70b6ce74a5f: Update doc typos and show `quick-start` tab as default. (#4577) (@zzjin) +* 4488fb61fdd4764e5f6a74213f7add08e4dc52fa: Update gen-apply-cluster.md (#4530) (@lamking) +* e87219f94e2a1135fb11e2ed4ec1aebb197c23b2: adapt ephemeral storage limit range (#4456) (@bxy4543) +* 055fe0bbc8103907e2e13d77311be4a05e0f10a6: adapt install script for cockroachdb. (#4593) (@lingdie) +* e45e3b79bd3805d659d5c4aae0370adc0fb32fb2: add cost service swagger (#4474) (@bxy4543) +* 8d6e23fd7c4810322575caf7c02f179b781f5abe: add init job (#4629) (@bxy4543) +* b12e933026792c720111a99c98c6b327e8a870c1: add limits to monitor service (#4652) (@nowinkeyy) +* 15279bcdae9412c8f57a155d5d65ae3bb3444e62: add minio service module to workflows (#4450) (@nowinkeyy) +* df38d07149d4d6839bfff414083dcf4077b986f1: add object storage key secret (#4623) (@nowinkeyy) +* 145ce7832f275a582c63101fb13c62f0b065d6a1: add pull file func (#4563) (@nowinkeyy) +* 9110edea0aaab5fa5cf8f5a58e9d6fb028e3ad48: change admin to username (#4557) (@nowinkeyy) +* cda0c5cba91f09780faaf57c79b91eb07c3afab7: create region (#4620) (@bxy4543) +* e7b1c54e2dc042328261773faaee020a2ed46bf9: docs: Add new blog (#4500) (@yangchuansheng) +* 972784f0047365de226cdc9ed8fcd879676eafc9: docs: Add templates documentation (#4458) (@yangchuansheng) +* 15fee3b856ca7f56be8a16cea7bb792ab8bbfc2c: docs: Update docs for installing sealos (#4660) (@yangchuansheng) +* 301d44f7dd51f2788508edcaa332053217d0162a: docs: add blog for affine (#4661) (@yangchuansheng) +* fee8721f53a2a6b18d0d3a1add1babb0dc2e4b30: docs: add component Highlight (#4471) (@yangchuansheng) +* b4daa2df1181598e396f0804b0d0a12fd16079f4: docs: add master services agreement (#4452) (@yangchuansheng) +* 43632605a34ad3ea3ee7e29ca234d0493679c327: docs: add static host doc (#4640) (@nowinkeyy) +* 9d606dc1f4c03a29a2fa4062f94d061339e6690b: docs: update lychee config (#4498) (@yangchuansheng) +* 5318703c0b64bc4e8e57548411593a40c5239aa1: docs:offline recharge promotions (#4575) (@zjy365) +* 87269b7997005ea27d0bb35a1eb0dfe234d9ab63: feat(object-storage):add sort feature (#4464) (@xudaotutou) +* 8abdf56a684331ed2fc7c754a9f34290f6c3f9a4: feat(sealos create): Add command preview functionality (#4291) (@LZiHaN) +* c3d58c03b2013ba6bda1b6853e738c55ad8dd424: fix create region (#4632) (@bxy4543) +* 2122fb726a2669b340a67be949e46081480f572d: fix env name error in deploy.yaml.tmpl (#4624) (@nowinkeyy) +* 60e490dbd8dea94881dbcf0f835fb35618342545: fix gen region uid (#4611) (@bxy4543) +* 60c481977f39e1565ff055e482913eaed15b93d2: fix install cli with proxy (#4465) (@bxy4543) +* d72a2b8a1d0b2d79fcaa753b2de07921a7acaa66: fix link 404 (#4664) (@nowinkeyy) +* 2113509f408f47621f62ae9dfe30c0e2e05582e5: fix link 404 err0r (#4669) (@nowinkeyy) +* 5e5e95c759c9d02c38086ac17b974bfaf44bb71b: fix minio init error (#4555) (@nowinkeyy) +* ae7d7edaefd7244224221a78385664b1f6e69cf2: fix object storage workflow error (#4542) (@nowinkeyy) +* e41b40f1b03a8906792ef19a40cc7b3873248eb1: fix scripts (#4612) (@bxy4543) +* 424fcc7275bf4735a4412fe2a084e309d6b591f0: fix user change ns annotations (#4592) (@bxy4543) +* b9418ed6b7ece3977ba078cd37a11926cf0c59b0: fix(object-storage):preview & copyURL (#4444) (@xudaotutou) +* 07fdde5bb10e01befc3283b982975bc5c0b9b7c4: k3s support version doc (#4455) (@bxy4543) +* c6e55b37dc22887d0e4d41003a40d974d73302ca: k3s support version doc (#4461) (@bxy4543) +* 2857df524bb018a104785062b9b5722ff9f0731f: optimize default kube runtime config; (#4487) (@bxy4543) +* e1aa395aaf4c827f8f0b96f7ca852b6c6b8ad3e1: optimize init minio sh (#4537) (@nowinkeyy) +* 5cb7e44a3cc829e04b0aa67595a8b49c433c7105: query traffic at a specified time (#4657) (@nowinkeyy) +* e6bf29b3c54b0bcf863532c57772ff7ce888eff9: refactor(costcenter): service api (#4475) (@xudaotutou) +* 70b1f5c6ecffe8c4301a06eb6ee2099ef0085835: refactor:license optimized deployment command form (#4532) (@zjy365) +* 9487f0be1d68edc99b3cb7e6caef7428ae5cb291: refactor:providers invite app (#4599) (@zjy365) +* 21f2c3a58aa601a55ba9358e699aef2f9e2adb7e: refine cluster image(objectstorage-controller、minio-service) (#4512) (@nowinkeyy) +* a0b3363d9880ecbfe61f47835e2e764c641c7ead: release: update sealos version in install.sh. (#4678) (@lingdie) +* 91142fd90b301c5ceb9a298ad6a5396637f543d7: style(kubepanel): adjust style (#4546) (@mlhiter) +* 2057bca8f047a934ff54e26eff123ee7b2644a8c: style(kubepanel): improve the overall style (#4494) (@mlhiter) +* 1fb9c119613f355077c2d37a59ba75de1c43349f: style: Update UI styles for AppLaunchpad (#4649) (@zjy365) +* 53631c6ef01d95e326eba89902626fd026090b3a: style: switch sealos coin (#4509) (@xudaotutou) +* 307e8682c0f8e7dd8a652bde19bf49c9428c9890: update cpu and memory config requirements for deploying sealos (#4625) (@nowinkeyy) +* 24417328a1ad238d9ace220eee644a2ed5f3923f: user registration switch (#4473) (@bxy4543) +* 967c5bf77d3515857074f34619289316a7d23884: 🤖 add release changelog using rebot. (#4442) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.0-beta4...v5.0.0-beta5 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG.md b/CHANGELOG/CHANGELOG.md index 82946aa3c5c..2a288f8d08b 100644 --- a/CHANGELOG/CHANGELOG.md +++ b/CHANGELOG/CHANGELOG.md @@ -2,6 +2,7 @@ All notable changes to this project will be documented in this file. +- [CHANGELOG-5.0.0-beta5.md](./CHANGELOG-5.0.0-beta5.md) - [CHANGELOG-5.0.0-beta4.md](./CHANGELOG-5.0.0-beta4.md) - [CHANGELOG-5.0.0-beta3.md](./CHANGELOG-5.0.0-beta3.md) - [CHANGELOG-5.0.0-beta2.md](./CHANGELOG-5.0.0-beta2.md) diff --git a/controllers/pkg/objectstorage/objectstorage.go b/controllers/pkg/objectstorage/objectstorage.go index 1f011ec8237..54e038dba7a 100644 --- a/controllers/pkg/objectstorage/objectstorage.go +++ b/controllers/pkg/objectstorage/objectstorage.go @@ -17,13 +17,19 @@ package objectstorage import ( "context" "fmt" + + "github.com/prometheus/prom2json" + "regexp" "strconv" "strings" "time" + "github.com/labring/sealos/controllers/pkg/utils/env" + "github.com/prometheus/common/model" + "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7" "github.com/prometheus/client_golang/api" v1 "github.com/prometheus/client_golang/api/prometheus/v1" @@ -44,6 +50,18 @@ func ListUserObjectStorageBucket(client *minio.Client, username string) ([]strin return expectBuckets, nil } +func ListAllObjectStorageBucket(client *minio.Client) ([]string, error) { + buckets, err := client.ListBuckets(context.Background()) + if err != nil { + return nil, err + } + var allBuckets []string + for _, bucket := range buckets { + allBuckets = append(allBuckets, bucket.Name) + } + return allBuckets, nil +} + func GetObjectStorageSize(client *minio.Client, bucket string) (int64, int64) { objects := client.ListObjects(context.Background(), bucket, minio.ListObjectsOptions{ Recursive: true, @@ -99,9 +117,11 @@ func GetUserObjectStorageFlow(client *minio.Client, promURL, username, instance return totalFlow, nil } +var timeoutDuration = time.Duration(env.GetInt64EnvWithDefault(EnvPromQueryObsTimeoutSecEnv, 10)) * time.Second + const ( - timeoutDuration = 5 * time.Second - timeFormat = "2006-01-02 15:04:05" + EnvPromQueryObsTimeoutSecEnv = "PROM_QUERY_OBS_TIMEOUT_SEC" + timeFormat = "2006-01-02 15:04:05" ) var ( @@ -164,3 +184,60 @@ func extractValues(result1, result2 model.Value) (int64, int64) { val2, _ := strconv.ParseInt(rcvdStr2, 10, 64) return val1, val2 } + +type MetricData struct { + // key: bucket name, value: usage + Usage map[string]int64 +} + +type Metrics map[string]MetricData + +func QueryUserUsage(client *madmin.MetricsClient) (Metrics, error) { + obMetrics := make(Metrics) + bucketMetrics, err := client.BucketMetrics(context.TODO()) + if err != nil { + return nil, fmt.Errorf("failed to get bucket metrics: %w", err) + } + for _, bucketMetric := range bucketMetrics { + if !isUsageBytesTargetMetric(bucketMetric.Name) { + continue + } + for _, metrics := range bucketMetric.Metrics { + promMetrics := metrics.(prom2json.Metric) + floatValue, err := strconv.ParseFloat(promMetrics.Value, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse %s to float value", promMetrics.Value) + } + intValue := int64(floatValue) + if bucket := promMetrics.Labels["bucket"]; bucket != "" { + user := getUserWithBucket(bucket) + metricData, exists := obMetrics[user] + if !exists { + metricData = MetricData{ + Usage: make(map[string]int64), + } + } + metricData.Usage[bucket] += intValue + obMetrics[user] = metricData + } + } + } + + return obMetrics, err +} + +func isUsageBytesTargetMetric(name string) bool { + targetMetrics := []string{ + "minio_bucket_usage_total_bytes", + } + for _, target := range targetMetrics { + if name == target { + return true + } + } + return false +} + +func getUserWithBucket(bucket string) string { + return strings.Split(bucket, "-")[0] +} diff --git a/controllers/pkg/utils/env/env.go b/controllers/pkg/utils/env/env.go index 657384df789..efa919abc66 100644 --- a/controllers/pkg/utils/env/env.go +++ b/controllers/pkg/utils/env/env.go @@ -27,6 +27,15 @@ func GetEnvWithDefault(key, defaultValue string) string { return defaultValue } +func GetBoolWithDefault(key string, defaultValue bool) bool { + if env, ok := os.LookupEnv(key); ok && env != "" { + if value, err := strconv.ParseBool(env); err == nil { + return value + } + } + return defaultValue +} + func GetInt64EnvWithDefault(key string, defaultValue int64) int64 { if env, ok := os.LookupEnv(key); ok && env != "" { if value, err := strconv.ParseInt(env, 10, 64); err == nil { diff --git a/controllers/resources/controllers/monitor_controller.go b/controllers/resources/controllers/monitor_controller.go index 475fa3d24d5..74850db26b9 100644 --- a/controllers/resources/controllers/monitor_controller.go +++ b/controllers/resources/controllers/monitor_controller.go @@ -21,9 +21,12 @@ import ( "fmt" "math" "os" + "strings" "sync" "time" + "github.com/minio/madmin-go/v3" + "golang.org/x/sync/errgroup" "github.com/labring/sealos/controllers/pkg/utils/env" @@ -61,18 +64,20 @@ import ( type MonitorReconciler struct { client.Client logr.Logger - Interval time.Duration - Scheme *runtime.Scheme - stopCh chan struct{} - wg sync.WaitGroup - periodicReconcile time.Duration - NvidiaGpu map[string]gpu.NvidiaGPU - DBClient database.Interface - TrafficClient database.Interface - Properties *resources.PropertyTypeLS - PromURL string - ObjStorageClient *minio.Client - ObjectStorageInstance string + Interval time.Duration + Scheme *runtime.Scheme + stopCh chan struct{} + wg sync.WaitGroup + periodicReconcile time.Duration + NvidiaGpu map[string]gpu.NvidiaGPU + DBClient database.Interface + TrafficClient database.Interface + Properties *resources.PropertyTypeLS + PromURL string + currentObjectMetrics map[string]objstorage.MetricData + ObjStorageClient *minio.Client + ObjStorageMetricsClient *madmin.MetricsClient + ObjectStorageInstance string } type quantity struct { @@ -131,7 +136,7 @@ func NewMonitorReconciler(mgr ctrl.Manager) (*MonitorReconciler, error) { func (r *MonitorReconciler) StartReconciler(ctx context.Context) error { r.startPeriodicReconcile() - if r.TrafficClient != nil { + if r.TrafficClient != nil || r.ObjStorageClient != nil { r.startMonitorTraffic() } <-ctx.Done() @@ -191,14 +196,14 @@ func (r *MonitorReconciler) startMonitorTraffic() { startTime, endTime := time.Now().UTC(), time.Now().Truncate(time.Hour).Add(1*time.Hour).UTC() waitNextHour() ticker := time.NewTicker(1 * time.Hour) - if err := r.MonitorPodTrafficUsed(startTime, endTime); err != nil { + if err := r.MonitorTrafficUsed(startTime, endTime); err != nil { r.Logger.Error(err, "failed to monitor pod traffic used") } for { select { case <-ticker.C: startTime, endTime = endTime, endTime.Add(1*time.Hour) - if err := r.MonitorPodTrafficUsed(startTime, endTime); err != nil { + if err := r.MonitorTrafficUsed(startTime, endTime); err != nil { r.Logger.Error(err, "failed to monitor pod traffic used") break } @@ -235,6 +240,9 @@ func (r *MonitorReconciler) processNamespaceList(namespaceList *corev1.Namespace r.Logger.Error(fmt.Errorf("no namespace to process"), "") return nil } + if err := r.preMonitorResourceUsage(); err != nil { + r.Logger.Error(err, "failed to pre monitor resource usage") + } sem := semaphore.NewWeighted(concurrentLimit) wg := sync.WaitGroup{} wg.Add(len(namespaceList.Items)) @@ -256,6 +264,15 @@ func (r *MonitorReconciler) processNamespaceList(namespaceList *corev1.Namespace return nil } +func (r *MonitorReconciler) preMonitorResourceUsage() error { + metrics, err := objstorage.QueryUserUsage(r.ObjStorageMetricsClient) + if err != nil { + return fmt.Errorf("failed to query object storage metrics: %w", err) + } + r.currentObjectMetrics = metrics + return nil +} + func (r *MonitorReconciler) monitorResourceUsage(namespace *corev1.Namespace) error { timeStamp := time.Now().UTC() podList := corev1.PodList{} @@ -381,41 +398,87 @@ func (r *MonitorReconciler) getObjStorageUsed(user string, namedMap *map[string] if len(buckets) == 0 { return nil } - for i := range buckets { - size, count := objstorage.GetObjectStorageSize(r.ObjStorageClient, buckets[i]) - if count == 0 || size == 0 { + if r.currentObjectMetrics == nil || r.currentObjectMetrics[user].Usage == nil { + return nil + } + for bucket, usage := range r.currentObjectMetrics[user].Usage { + if bucket == "" || usage <= 0 { continue } - end := time.Now().Truncate(time.Minute) - // get the traffic of the last minute - bytes, err := objstorage.GetObjectStorageFlow(r.PromURL, buckets[i], r.ObjectStorageInstance, end.Add(-time.Minute), end) - if err != nil { - return fmt.Errorf("failed to get object storage user storage flow: %w", err) - } - objStorageNamed := resources.NewObjStorageResourceNamed(buckets[i]) + objStorageNamed := resources.NewObjStorageResourceNamed(bucket) (*namedMap)[objStorageNamed.String()] = objStorageNamed if _, ok := (*resMap)[objStorageNamed.String()]; !ok { (*resMap)[objStorageNamed.String()] = initResources() } - (*resMap)[objStorageNamed.String()][corev1.ResourceStorage].Add(*resource.NewQuantity(size, resource.BinarySI)) - //If object storage traffic bytes is smaller than 0.1 MB, no record is recorded - if bytes >= 100*1024 { - (*resMap)[objStorageNamed.String()][resources.ResourceNetwork].Add(*resource.NewQuantity(bytes, resource.BinarySI)) - } + (*resMap)[objStorageNamed.String()][corev1.ResourceStorage].Add(*resource.NewQuantity(usage, resource.BinarySI)) } return nil } -func (r *MonitorReconciler) MonitorPodTrafficUsed(startTime, endTime time.Time) error { - logger.Info("start getPodTrafficUsed", "startTime", startTime.Format(time.RFC3339), "endTime", endTime.Format(time.RFC3339)) +func (r *MonitorReconciler) MonitorTrafficUsed(startTime, endTime time.Time) error { + logger.Info("start getTrafficUsed", "startTime", startTime.Format(time.RFC3339), "endTime", endTime.Format(time.RFC3339)) execTime := time.Now().UTC() - if err := r.monitorPodTrafficUsed(startTime, endTime); err != nil { - r.Logger.Error(err, "failed to monitor pod traffic used") + if r.TrafficClient != nil { + if err := r.monitorPodTrafficUsed(startTime, endTime); err != nil { + r.Logger.Error(err, "failed to monitor pod traffic used") + } + } + if r.ObjStorageClient != nil { + if err := r.monitorObjectStorageTrafficUsed(startTime, endTime); err != nil { + r.Logger.Error(err, "failed to monitor object storage traffic used") + } } r.Logger.Info("success to monitor pod traffic used", "startTime", startTime.Format(time.RFC3339), "endTime", endTime.Format(time.RFC3339), "execTime", time.Since(execTime).String()) return nil } +func (r *MonitorReconciler) monitorObjectStorageTrafficUsed(startTime, endTime time.Time) error { + buckets, err := objstorage.ListAllObjectStorageBucket(r.ObjStorageClient) + if err != nil { + return fmt.Errorf("failed to list object storage buckets: %w", err) + } + r.Logger.Info("object storage buckets", "buckets len", len(buckets)) + wg, _ := errgroup.WithContext(context.Background()) + wg.SetLimit(10) + for i := range buckets { + bucket := buckets[i] + if !strings.Contains(bucket, "-") { + continue + } + wg.Go(func() error { + return r.handlerObjectStorageTrafficUsed(startTime, endTime, bucket) + }) + } + return wg.Wait() +} + +func (r *MonitorReconciler) handlerObjectStorageTrafficUsed(startTime, endTime time.Time, bucket string) error { + bytes, err := objstorage.GetObjectStorageFlow(r.PromURL, bucket, r.ObjectStorageInstance, startTime, endTime) + if err != nil { + return fmt.Errorf("failed to get object storage flow: %w", err) + } + unit := r.Properties.StringMap[resources.ResourceNetwork].Unit + used := int64(math.Ceil(float64(resource.NewQuantity(bytes, resource.BinarySI).MilliValue()) / float64(unit.MilliValue()))) + if used <= 0 { + return nil + } + + namespace := "ns-" + strings.SplitN(bucket, "-", 2)[0] + ro := resources.Monitor{ + Category: namespace, + Name: bucket, + Used: map[uint8]int64{r.Properties.StringMap[resources.ResourceNetwork].Enum: used}, + Time: endTime.Add(-1 * time.Minute), + Type: resources.AppType[resources.ObjectStorage], + } + r.Logger.Info("object storage traffic used", "monitor", ro) + err = r.DBClient.InsertMonitor(context.Background(), &ro) + if err != nil { + return fmt.Errorf("failed to insert monitor: %w", err) + } + return nil +} + func (r *MonitorReconciler) monitorPodTrafficUsed(startTime, endTime time.Time) error { monitors, err := r.DBClient.GetDistinctMonitorCombinations(startTime, endTime) if err != nil { diff --git a/controllers/resources/main.go b/controllers/resources/main.go index 0bd1829ee4a..3f29408be1f 100644 --- a/controllers/resources/main.go +++ b/controllers/resources/main.go @@ -22,6 +22,10 @@ import ( "os" "time" + "github.com/labring/sealos/controllers/pkg/utils/env" + + "github.com/minio/madmin-go/v3" + "github.com/labring/sealos/controllers/pkg/database/mongo" "github.com/labring/sealos/controllers/pkg/database" @@ -145,12 +149,14 @@ func main() { } reconciler.Properties = resources.DefaultPropertyTypeLS const ( - MinioEndpoint = "MINIO_ENDPOINT" - MinioAk = "MINIO_AK" - MinioSk = "MINIO_SK" - PromURL = "PROM_URL" + MinioEndpoint = "MINIO_ENDPOINT" + MinioAk = "MINIO_AK" + MinioSk = "MINIO_SK" + PromURL = "PROM_URL" + MinioMetricsAddr = "MINIO_METRICS_ADDR" + MinioMetricsAddrSecure = "MINIO_METRICS_SECURE" ) - if endpoint, ak, sk := os.Getenv(MinioEndpoint), os.Getenv(MinioAk), os.Getenv(MinioSk); endpoint != "" && ak != "" && sk != "" { + if endpoint, ak, sk, mAddr := os.Getenv(MinioEndpoint), os.Getenv(MinioAk), os.Getenv(MinioSk), os.Getenv(MinioMetricsAddr); endpoint != "" && ak != "" && sk != "" && mAddr != "" { reconciler.Logger.Info("init minio client") if reconciler.ObjStorageClient, err = objectstoragev1.NewOSClient(endpoint, ak, sk); err != nil { reconciler.Logger.Error(err, "failed to new minio client") @@ -161,13 +167,18 @@ func main() { reconciler.Logger.Error(err, "failed to list minio buckets") os.Exit(1) } - if promURL := os.Getenv(PromURL); promURL == "" { + if reconciler.PromURL = os.Getenv(PromURL); reconciler.PromURL == "" { reconciler.Logger.Info("prometheus url not found, please check env: PROM_URL") - } else { - reconciler.PromURL = promURL } + secure := env.GetBoolWithDefault(MinioMetricsAddrSecure, false) + reconciler.ObjStorageMetricsClient, err = madmin.NewMetricsClient(mAddr, ak, sk, secure) + if err != nil { + reconciler.Logger.Error(err, "failed to new minio metrics client") + os.Exit(1) + } + reconciler.Logger.Info("init minio client with info (endpoint %s, metrics addr %s, metrics addr secure %v) success", endpoint, mAddr, secure) } else { - reconciler.Logger.Info("minio info not found, please check env: MINIO_ENDPOINT, MINIO_AK, MINIO_SK") + reconciler.Logger.Info("minio info not found, please check env: MINIO_ENDPOINT, MINIO_AK, MINIO_SK, MINIO_METRICS_ADDR") } // timer creates tomorrow's timing table in advance to ensure that tomorrow's table exists // Execute immediately and then every 24 hours. diff --git a/deploy/cloud/manifests/cockroachdb.yaml b/deploy/cloud/manifests/cockroachdb.yaml index 7719529ce6c..4395929693e 100644 --- a/deploy/cloud/manifests/cockroachdb.yaml +++ b/deploy/cloud/manifests/cockroachdb.yaml @@ -21,6 +21,5 @@ spec: cpu: 1000m memory: 2Gi tlsEnabled: true - image: - name: docker.io/cockroachdb/cockroach:v23.1.11 + cockroachDBVersion: v23.1.11 nodes: 3 \ No newline at end of file diff --git a/docs/4.0/docs/self-hosting/sealos/installation.md b/docs/4.0/docs/self-hosting/sealos/installation.md index 5daff00b707..74725f792de 100644 --- a/docs/4.0/docs/self-hosting/sealos/installation.md +++ b/docs/4.0/docs/self-hosting/sealos/installation.md @@ -90,7 +90,7 @@ It works by taking any IP address as part of a `nip.io` subdomain, and resolving To use nip.io for Sealos, run the below on the first master node and enter prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/main/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh ``` When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP. @@ -127,7 +127,7 @@ This maps your domain and subdomains to the first master's public IP. Then run below on the first master, entering prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/main/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ --cloud-domain= \ --cert-path= \ --key-path= @@ -149,7 +149,7 @@ cloud.example.io A Then run the below on the first master, entering prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/main/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ --cloud-domain= ``` @@ -206,7 +206,7 @@ This resolves `cloud.example.io` and subdomains to the first master internal IP. Then run below on the first master, entering prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/main/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ --cloud-domain= ``` diff --git a/docs/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md b/docs/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md index c8462e8e3e7..34755fe2860 100644 --- a/docs/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md +++ b/docs/4.0/i18n/zh-Hans/self-hosting/sealos/installation.md @@ -101,8 +101,8 @@ Sealos 需要使用证书来保证通信安全,默认在您不提供证书的 使用 nip.io 作为 Sealos 的域名非常简单,只需在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta4/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0-beta4 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.0-beta5 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com ``` @@ -145,8 +145,8 @@ cloud.example.io A 192.168.1.10 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta4/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0-beta4 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.0-beta5 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= \ @@ -172,8 +172,8 @@ cloud.example.io A 192.168.1.10 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta4/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0-beta4 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.0-beta5 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= @@ -231,8 +231,8 @@ $ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta4/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0-beta4 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ + --cloud-version=v5.0.0-beta5 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 98ba347a810..005d2bcfe7c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -20,7 +20,7 @@ WORKDIR /app FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat && npm install -g pnpm +RUN apk add --no-cache libc6-compat && corepack enable && corepack prepare pnpm@8.9.0 --activate # Install dependencies based on the preferred package manager root workspace diff --git a/frontend/desktop/README.md b/frontend/desktop/README.md index a730cae4027..a284205c4b5 100644 --- a/frontend/desktop/README.md +++ b/frontend/desktop/README.md @@ -197,22 +197,23 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the - 登录功能的开关, 部署时要用`true`配置想要使用的登录方式。 - ``` - WECHAT_ENABLED=true - GITHUB_ENABLED=true - PASSWORD_ENABLED=true - SMS_ENABLED=true - RECHAGRE_ENABLED=true - ``` + ``` + WECHAT_ENABLED=true + GITHUB_ENABLED=true + PASSWORD_ENABLED=true + SMS_ENABLED=true + RECHAGRE_ENABLED=true + ``` - 每个登陆要配置的变量 + - wechat ``` WECHAT_CLIENT_ID= WECHAT_CLIENT_SECRET= WECHAT_ENABLED="true" - ``` + ``` - github @@ -238,15 +239,29 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the ALI_TEMPLATE_CODE= SMS_ENABLED="true" ``` + - google + ``` GOOGLE_ENABLED="true" GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` - - team - + + - support standard oauth2 + + ``` + OAUTH2_CLIENT_ID= + OAUTH2_CLIENT_SECRET= + OAUTH2_AUTH_URL= + OAUTH2_TOKEN_URL= + OAUTH2_USERINFO_URL= + ``` + + - number of teams and number of people in each team + ``` // default is '50' TEAM_LIMIT="50" - ``` \ No newline at end of file + TEAM_INVITE_LIMIT="50" + ``` diff --git a/frontend/desktop/src/pages/api/dev/migrateInvoice.ts b/frontend/desktop/src/pages/api/dev/migrateInvoice.ts new file mode 100644 index 00000000000..711918be6b6 --- /dev/null +++ b/frontend/desktop/src/pages/api/dev/migrateInvoice.ts @@ -0,0 +1,90 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { MongoClient, WithId } from 'mongodb'; +import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { Prisma } from '@prisma/client/extension'; + +export type TInvoiceDetail = { + title: string; + tax: string; + bank: string; + bankAccount: string; + address?: string; + phone?: string; + fax?: string; +}; +export type TInvoiceContract = { + person: string; + phone: string; + email: string; +}; + +export type Tbilling = { + order_id: string; + amount: number; + createdTime: Date; +}; + +export type InvoicesCollection = { + amount: number; + detail: TInvoiceDetail; + billings: Tbilling[]; + contract: TInvoiceContract; + k8s_user: string; + createdTime: Date; +}; +const mongodb = new MongoClient(process.env.MONGODB_URI_COSTCENTER!); +const client = mongodb.db(); + +async function pullInvoiceData() { + const invoicesCollection = client.collection('invoices'); + const billings = await invoicesCollection + .aggregate([ + { $unwind: '$billings' }, + { + $replaceRoot: { + newRoot: '$billings' + } + } + ]) + .toArray(); + const data = billings.map((b) => b.order_id); + // console.log(data) + const result = await globalPrisma.$transaction(async (tx) => { + const existData = await tx.payment.findMany({ + where: { + id: { + in: data + }, + invoiced_at: false + }, + select: { + id: true + } + }); + console.log(existData); + return await tx.payment.updateMany({ + where: { + id: { + in: existData.map((d) => d.id) + } + }, + data: { + invoiced_at: true + } + }); + }); + console.log(result); +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (process.env.migrate !== 'true') return; + console.log('pullInvoice'); + await pullInvoiceData(); + console.log('ok'); + return res.json('ok'); + } catch (error) { + console.log(error); + return res.json('error'); + } +} diff --git a/frontend/desktop/src/stores/app.ts b/frontend/desktop/src/stores/app.ts index 75e29642597..dcf50c49b91 100644 --- a/frontend/desktop/src/stores/app.ts +++ b/frontend/desktop/src/stores/app.ts @@ -119,7 +119,7 @@ const useAppStore = create()( }); }, - openApp: async (app: TApp, { query, raw, pathname = '/' } = {}) => { + openApp: async (app: TApp, { query, raw, pathname = '/', appSize = 'maximize' } = {}) => { const zIndex = get().maxZIndex + 1; // debugger // 未支持多实例 @@ -140,7 +140,7 @@ const useAppStore = create()( let run_app = get().runner.openApp(app.key); const _app = new AppInfo(app, run_app.pid); _app.zIndex = zIndex; - _app.size = 'maximize'; + _app.size = appSize; _app.isShow = true; // add query to url if (_app.data?.url) { diff --git a/frontend/desktop/src/types/app.ts b/frontend/desktop/src/types/app.ts index 5b96bb6a022..22b7813dab8 100644 --- a/frontend/desktop/src/types/app.ts +++ b/frontend/desktop/src/types/app.ts @@ -72,6 +72,7 @@ export type TOSState = { query?: Record; raw?: string; pathname?: string; + appSize?: WindowSize; } ): Promise; // close app diff --git a/frontend/packages/driver/package.json b/frontend/packages/driver/package.json index 565683fa681..5f2933fa40a 100644 --- a/frontend/packages/driver/package.json +++ b/frontend/packages/driver/package.json @@ -6,7 +6,8 @@ "src": "./src", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "next dev" + "dev": "next dev", + "build": "echo \"build driver\"" }, "keywords": [ "driver", diff --git a/frontend/packages/ui/package.json b/frontend/packages/ui/package.json index 450ff8e1604..1facdeb7eed 100644 --- a/frontend/packages/ui/package.json +++ b/frontend/packages/ui/package.json @@ -6,7 +6,8 @@ "src": "./src", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "next dev" + "dev": "next dev", + "build": "echo \"build @sealos/ui\"" }, "keywords": [], "author": "", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 78f5f44fb2f..2fb5751893e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -686,7 +686,7 @@ importers: version: 5.9.1 next: specifier: 13.1.6 - version: 13.1.6(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + version: 13.1.6(@babel/core@7.23.3)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) next-i18next: specifier: ^13.3.0 version: 13.3.0(i18next@22.5.1)(next@13.1.6)(react-i18next@12.3.1)(react@18.2.0) @@ -13108,7 +13108,7 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 dependencies: - '@babel/runtime': 7.23.4 + '@babel/runtime': 7.24.0 aria-query: 5.3.0 array-includes: 3.1.7 array.prototype.flatmap: 1.3.2 @@ -17345,51 +17345,6 @@ packages: - babel-plugin-macros dev: false - /next@13.1.6(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5): - resolution: {integrity: sha512-hHlbhKPj9pW+Cymvfzc15lvhaOZ54l+8sXDXJWm3OBNBzgrVj6hwGPmqqsXg40xO1Leq+kXpllzRPuncpC0Phw==} - engines: {node: '>=14.6.0'} - hasBin: true - peerDependencies: - fibers: '>= 3.1.0' - node-sass: ^6.0.0 || ^7.0.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - peerDependenciesMeta: - fibers: - optional: true - node-sass: - optional: true - sass: - optional: true - dependencies: - '@next/env': 13.1.6 - '@swc/helpers': 0.4.14 - caniuse-lite: 1.0.30001565 - postcss: 8.4.14 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - sass: 1.69.5 - styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) - optionalDependencies: - '@next/swc-android-arm-eabi': 13.1.6 - '@next/swc-android-arm64': 13.1.6 - '@next/swc-darwin-arm64': 13.1.6 - '@next/swc-darwin-x64': 13.1.6 - '@next/swc-freebsd-x64': 13.1.6 - '@next/swc-linux-arm-gnueabihf': 13.1.6 - '@next/swc-linux-arm64-gnu': 13.1.6 - '@next/swc-linux-arm64-musl': 13.1.6 - '@next/swc-linux-x64-gnu': 13.1.6 - '@next/swc-linux-x64-musl': 13.1.6 - '@next/swc-win32-arm64-msvc': 13.1.6 - '@next/swc-win32-ia32-msvc': 13.1.6 - '@next/swc-win32-x64-msvc': 13.1.6 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - dev: false - /next@13.2.4(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5): resolution: {integrity: sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==} engines: {node: '>=14.6.0'} @@ -17418,7 +17373,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) sass: 1.69.5 - styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) optionalDependencies: '@next/swc-android-arm-eabi': 13.2.4 '@next/swc-android-arm64': 13.2.4 @@ -17509,7 +17464,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) sass: 1.69.5 - styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) watchpack: 2.4.0 zod: 3.21.4 optionalDependencies: @@ -17549,7 +17504,7 @@ packages: postcss: 8.4.31 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.23.5)(react@18.2.0) + styled-jsx: 5.1.1(@babel/core@7.23.3)(react@18.2.0) watchpack: 2.4.0 optionalDependencies: '@next/swc-darwin-arm64': 13.5.4 diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx index 1b41c6173c4..02f6247de5d 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/Pods.tsx @@ -12,6 +12,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons'; import { Box, Button, + Text, Center, Flex, Table, @@ -127,8 +128,22 @@ const Pods = ({ title: 'Cpu', key: 'cpu', render: (item: PodDetailType) => ( - - + + + + + {item?.usedCpu?.yData[item?.usedCpu?.yData?.length - 1]}% + + ) }, @@ -136,8 +151,22 @@ const Pods = ({ title: 'Memory', key: 'memory', render: (item: PodDetailType) => ( - - + + + + + {item?.usedMemory?.yData[item?.usedMemory?.yData?.length - 1]}% + + ) }, diff --git a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx index 91f2513cbfb..824aa34d8c4 100644 --- a/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx +++ b/frontend/providers/applaunchpad/src/pages/apps/components/appList.tsx @@ -11,7 +11,7 @@ import { useGlobalStore } from '@/store/global'; import { useUserStore } from '@/store/user'; import { AppListItemType } from '@/types/app'; import { getErrText } from '@/utils/tools'; -import { Box, Button, Center, Flex, MenuButton, useTheme } from '@chakra-ui/react'; +import { Box, Text, Button, Center, Flex, MenuButton, useTheme } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; @@ -143,6 +143,18 @@ const AppList = ({ + + {item?.usedCpu?.yData[item?.usedCpu?.yData?.length - 1]}% + ) @@ -154,6 +166,18 @@ const AppList = ({ + + {item?.usedMemory?.yData[item?.usedMemory?.yData?.length - 1]}% + ) diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index 14dbf41d204..a9a63a0c5b7 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -24,6 +24,7 @@ "Deduction": "扣费", "Total Cost": "总成本", "Cost Distribution": "成本分布", + "Income And Expense": "收支", "Expenditure": "支出", "Recharge Amount": "充值金额", "Select Amount": "选择金额", diff --git a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx index 4bd8cfc6a1d..40840d95b9e 100644 --- a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx @@ -91,7 +91,8 @@ export default function RechargeTabPanel() { id: TableHeaderID.TransactionTime, enablePinning: true, cell(props) { - return format(parseISO(props.cell.getValue()), 'MM-dd HH:mm'); + const date = new Date(props.cell.getValue()); + return format(date, 'MM-dd HH:mm'); } }), columnHelper.accessor((row) => row.Amount, { diff --git a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx index e196ca32767..b18d75d7c46 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx @@ -41,7 +41,9 @@ export function Buget() { return ( - {t('Income And Expense')} + + {t('Income And Expense')} + {list.map((v) => ( diff --git a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx index 66e4f066bd3..6440e08ad52 100644 --- a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx +++ b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx @@ -2,9 +2,10 @@ import { InvoiceTableHeaders } from '@/constants/billing'; import { Checkbox, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react'; import { format } from 'date-fns'; import { useTranslation } from 'next-i18next'; -import { ReqGenInvoice } from '@/types'; +import { RechargeBillingItem, ReqGenInvoice } from '@/types'; import CurrencySymbol from '../CurrencySymbol'; import useEnvStore from '@/stores/env'; +import { formatMoney } from '@/utils/format'; export function InvoiceTable({ data, @@ -13,7 +14,7 @@ export function InvoiceTable({ }: { data: ReqGenInvoice['billings']; selectbillings: ReqGenInvoice['billings']; - onSelect?: (type: boolean, item: ReqGenInvoice['billings'][0]) => void; + onSelect?: (type: boolean, item: RechargeBillingItem) => void; }) { const { t } = useTranslation(); const needSelect = !!onSelect; @@ -37,7 +38,11 @@ export function InvoiceTable({ > {t(item)} - {item !== 'Order Number' && } + {item === 'True Amount' && ( + <> + () + + )} ))} @@ -45,12 +50,12 @@ export function InvoiceTable({ {data.map((item) => ( - + {needSelect && ( b.order_id).includes(item.order_id)} + isChecked={selectbillings.map((b) => b.ID).includes(item.ID)} onChange={(v) => { onSelect(v.target.checked, item); }} @@ -59,11 +64,11 @@ export function InvoiceTable({ h="12px" /> )} - {item.order_id} + {item.ID} - {format(item.createdTime, 'MM-dd HH:mm')} - {item.amount} + {format(new Date(item.CreatedAt), 'MM-dd HH:mm')} + {item.Amount} ))} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/billings.ts b/frontend/providers/costcenter/src/pages/api/invoice/billings.ts deleted file mode 100644 index 562bac94e33..00000000000 --- a/frontend/providers/costcenter/src/pages/api/invoice/billings.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/service/backend/response'; -import { authSession } from '@/service/backend/auth'; -import { getAllInvoicesByK8sUser } from '@/service/backend/db/invoice'; -import { enableInvoice } from '@/service/enabled'; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (!enableInvoice()) { - throw new Error('invoice is not enabled'); - } - const kc = await authSession(req.headers); - const user = kc.getCurrentUser(); - if (user === null) { - return jsonRes(res, { code: 401, message: 'user null' }); - } - const invoices = await getAllInvoicesByK8sUser({ k8s_user: user.name }); - - return jsonRes(res, { - message: 'successfully', - data: { - billings: invoices.flatMap((invoice) => invoice.billings.map((billing) => billing.order_id)) - }, - code: 200 - }); - } catch (error) { - console.log(error); - jsonRes(res, { - message: 'Failed to get billings', - code: 500 - }); - } -} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts index a8d67fd11be..1d2d2006af8 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts @@ -1,21 +1,32 @@ import { NextApiRequest, NextApiResponse } from 'next'; -// import twilio from 'twilio'; -//@ts-ignore import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525'; -//@ts-ignore import * as OpenApi from '@alicloud/openapi-client'; -//@ts-ignore import * as Util from '@alicloud/tea-util'; import { jsonRes } from '@/service/backend/response'; import { addOrUpdateCode, checkSendable } from '@/service/backend/db/verifyCode'; -import { retrySerially } from '@/utils/tools'; +import { getClientIPFromRequest, retrySerially } from '@/utils/tools'; import { authSession } from '@/service/backend/auth'; import { enableInvoice } from '@/service/enabled'; +import * as process from 'process'; const accessKeyId = process.env.ALI_ACCESS_KEY_ID; const accessKeySecret = process.env.ALI_ACCESS_KEY_SECRET; const templateCode = process.env.ALI_TEMPLATE_CODE; const signName = process.env.ALI_SIGN_NAME; +const requestTimestamps: Record = {}; +function checkRequestFrequency(ipAddress: string) { + const currentTime = Date.now(); + const lastRequestTime = requestTimestamps[ipAddress] || 0; + const timeDiff = currentTime - lastRequestTime; + const requestInterval = 60 * 1000; + + if (timeDiff < requestInterval) { + return false; + } else { + requestTimestamps[ipAddress] = currentTime; + return true; + } +} export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { if (!enableInvoice()) { @@ -27,10 +38,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return jsonRes(res, { code: 401, message: 'user null' }); } const { phoneNumbers } = req.body; - if (!(await checkSendable(phoneNumbers))) { + let ip = getClientIPFromRequest(req); + + if (!ip) { + if (process.env.NODE_ENV === 'development') ip = '127.0.0.1'; + else + return jsonRes(res, { + message: 'The IP is null', + code: 403 + }); + } + if ( + !(await checkSendable({ + phone: phoneNumbers, + ip + })) + ) { return jsonRes(res, { - message: 'code already sent', - code: 400 + message: 'Code already sent', + code: 429 }); } @@ -76,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, 3); // update cache - await addOrUpdateCode({ phone: phoneNumbers, code }); + await addOrUpdateCode({ phone: phoneNumbers, code, ip }); return jsonRes(res, { message: 'successfully', code: 200 diff --git a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts index abfc96939be..ef5e2ba2e6d 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts @@ -26,6 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (user === null) { return jsonRes(res, { code: 401, message: 'user null' }); } + const { detail, contract, billings } = req.body as ReqGenInvoice; if ( !detail || @@ -44,6 +45,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 400 }); } + const url = process.env.BILLING_URI + '/account/v1alpha1/payment/set-invoice'; + const setInvoiceRes = await fetch(url, { + method: 'POST', + body: JSON.stringify({ + kubeConfig: kc.exportConfig(), + owner: user.name, + paymentIDList: billings.map((b) => b.ID) + }) + }); + if (!setInvoiceRes.ok) throw Error('setInvocice error'); if ( process.env.NODE_ENV !== 'development' && !(await checkCode({ phone: contract.phone, code: contract.code })) @@ -70,8 +81,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) detail, contract, billings: billings.map((item) => ({ - ...item, - createdTime: new Date(item.createdTime) + order_id: item.ID, + amount: item.Amount, + regionUID: item.RegionUID, + userUID: item.UserUID, + createdTime: new Date(item.CreatedAt) })) }; @@ -85,7 +99,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 500 }); } - retrySerially(async () => { + await retrySerially(async () => { try { const result = await sendToBot(document); if (result.StatusCode !== 0) { diff --git a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx index 458647293dc..8a61061ab8b 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx @@ -39,6 +39,7 @@ import arrow_icon from '@/assert/Vector.svg'; import email_icon from '@/assert/mdi_email-receive-outline.svg'; import listIcon from '@/assert/list.svg'; import { InvoiceTable } from '@/components/invoice/invoiceTable'; + const BillingModal = ({ billings, t, @@ -55,17 +56,13 @@ const BillingModal = ({ const [currentPage, setcurrentPage] = useState(1); return ( <> - {' '} - {' '} - {' '} - {t('orders.invoiceOrder')}({invoiceCount}){' '} - ¥ {invoiceAmount}{' '} - {' '} - {' '} + {t('orders.invoiceOrder')}({invoiceCount}) + ¥ {invoiceAmount} + + - {' '} - {' '} - {' '} - {' '} - {' '} - {t('orders.list')}{' '} - {' '} - {' '} + + {t('orders.list')} + + index <= pageSize * currentPage - 1 && index >= pageSize * (currentPage - 1) )} - >{' '} - {' '} + > + - {' '} - {t('Total')}: {billings?.current?.length || 0}{' '} + {t('Total')}: {billings?.current?.length || 0} - {' '} {' '} + + {' '} - {currentPage}/{totalPage}{' '} + + + + {currentPage}/{totalPage} + {' '} + + {' '} - {' '} - {pageSize} /{t('Page')}{' '} - {' '} - {' '} - {' '} + + + + {pageSize} /{t('Page')} + + + ); }; + function InvoicdForm({ invoiceAmount, invoiceCount, diff --git a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx index 13580cb2d77..be564344902 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx @@ -1,35 +1,34 @@ import { InvoiceTable } from '@/components/invoice/invoiceTable'; import { Box, Button, Flex, Heading, Img, Input, Text } from '@chakra-ui/react'; import { useEffect, useRef, useState } from 'react'; -import { endOfDay, formatISO, parseISO } from 'date-fns'; import receipt_icon from '@/assert/invoice-active.svg'; import arrow_icon from '@/assert/Vector.svg'; import arrow_left_icon from '@/assert/toleft.svg'; import magnifyingGlass_icon from '@/assert/magnifyingGlass.svg'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import request from '@/service/request'; -import { BillingData, BillingSpec } from '@/types/billing'; +import { + BillingData, + BillingSpec, + RechargeBillingData, + RechargeBillingItem +} from '@/types/billing'; import SelectRange from '@/components/billing/selectDateRange'; import useOverviewStore from '@/stores/overview'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'next-i18next'; -import { getCookie } from '@/utils/cookieUtils'; import NotFound from '@/components/notFound'; -import { formatMoney } from '@/utils/format'; import listIcon from '@/assert/list.svg'; -import { ReqGenInvoice } from '@/types'; +import { ApiResp, ReqGenInvoice } from '@/types'; import InvoicdForm from './InvoicdForm'; import { enableInvoice } from '@/service/enabled'; +import { formatMoney } from '@/utils/format'; function Invoice() { const { t, i18n } = useTranslation(); - const cookie = getCookie('NEXT_LOCALE'); - useEffect(() => { - i18n.changeLanguage(cookie); - }, [cookie, i18n]); const startTime = useOverviewStore((state) => state.startTime); const endTime = useOverviewStore((state) => state.endTime); - const selectBillings = useRef([]); + const selectBillings = useRef([]); const [searchValue, setSearch] = useState(''); const [orderID, setOrderID] = useState(''); const [totalPage, setTotalPage] = useState(1); @@ -39,64 +38,35 @@ function Invoice() { const [invoiceAmount, setInvoiceAmount] = useState(0); const [processState, setProcessState] = useState(0); const [invoiceCount, setInvoiceCount] = useState(0); - const { data: filterData } = useQuery(['billing', 'invoice'], () => { - return request('/api/invoice/billings'); - }); const { data, isLoading, isSuccess } = useQuery( - ['billing', { currentPage, startTime, endTime, orderID }], - async () => { - let spec = {} as BillingSpec; - spec = { - page: currentPage, - pageSize: pageSize, - type: 1, - startTime: formatISO(startTime, { representation: 'complete' }), - // startTime, - endTime: formatISO(endOfDay(endTime), { representation: 'complete' }), - // endTime, - orderID - }; - const result = await request( - '/api/billing', - { - method: 'POST', - data: { - spec - } - } - ); - - const tableResult = result.data.status.item - .filter((billing) => billing.type === 1) - .map((billing) => ({ - createdTime: parseISO(billing.time).getTime(), - order_id: billing.order_id, - amount: formatMoney(billing.payment?.amount || billing.amount) - })); - return { - tableResult, - pageLength: result.data.status.pageLength, - totalCount: result.data.status.totalCount || tableResult.length - }; + [ + 'billing', + 'invoice', + { + startTime, + endTime + } + ], + () => { + return request>('/api/billing/recharge', { + data: { + startTime, + endTime + }, + method: 'POST' + }); }, { - onSuccess(data) { - const totalPage = data.pageLength; - if (totalPage === 0) { - // 搜索时 - setTotalPage(1); - return; - } - setTotalPage(totalPage); - }, - staleTime: 1000, - cacheTime: 0, - enabled: filterData !== undefined + select(data) { + return ((data?.data?.payment || []) as RechargeBillingItem[]) + .filter((d) => !d.InvoicedAt) + .map((d) => ({ + ...d, + Amount: formatMoney(d.Amount) + })); + } } ); - const billingFilter = (billing: T): boolean => - !(filterData?.data.billings || []).includes(billing.order_id); - let tableResult = data?.tableResult?.filter(billingFilter) || []; return ( @@ -164,7 +134,7 @@ function Invoice() { - {isSuccess && tableResult.length > 0 ? ( + {isSuccess ? ( <> { if (checked) { - setInvoiceAmount(invoiceAmount + item.amount); + setInvoiceAmount(invoiceAmount + item.Amount); setInvoiceCount(invoiceCount + 1); selectBillings.current.push({ ...item }); } else { - setInvoiceAmount(invoiceAmount - item.amount); + setInvoiceAmount(invoiceAmount - item.Amount); setInvoiceCount(invoiceCount - 1); const idx = selectBillings.current.findIndex( - (billing) => billing.order_id === item.order_id + (billing) => billing.ID === item.ID ); selectBillings.current.splice(idx, 1); } @@ -224,7 +194,7 @@ function Invoice() { {t('Total')}: - {data.totalCount - (filterData?.data?.billings || []).length} + {data.length} - {currentPage}/{totalPage} + + {currentPage}/{totalPage} +