diff --git a/core/clustersmngr/clustersmngr.go b/core/clustersmngr/clustersmngr.go
new file mode 100644
index 0000000000..2299a0df81
--- /dev/null
+++ b/core/clustersmngr/clustersmngr.go
@@ -0,0 +1,92 @@
+package clustersmngr
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/weaveworks/weave-gitops/pkg/kube"
+ "github.com/weaveworks/weave-gitops/pkg/server/auth"
+ "k8s.io/client-go/rest"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
+
+type key int
+
+const (
+ // Clusters Clients Pool context key
+ ClustersClientsPoolCtxKey key = iota
+)
+
+var (
+ scheme = kube.CreateScheme()
+)
+
+// Cluster defines a leaf cluster
+type Cluster struct {
+ // Name defines the cluster name
+ Name string `yaml:"name"`
+ // Server defines cluster api address
+ Server string `yaml:"server"`
+
+ // SecretRef defines secret name that holds the cluster Bearer Token
+ SecretRef string `yaml:"secretRef"`
+ // BearerToken cluster access token read from SecretRef
+ BearerToken string
+
+ // TLSConfig holds configuration for TLS connection with the cluster values read from SecretRef
+ TLSConfig rest.TLSClientConfig
+}
+
+//ClusterFetcher fetches all leaf clusters
+//counterfeiter:generate . ClusterFetcher
+type ClusterFetcher interface {
+ Fetch(ctx context.Context) ([]Cluster, error)
+}
+
+// ClientsPool stores all clients to the leaf clusters
+type ClientsPool interface {
+ Add(user *auth.UserPrincipal, cluster Cluster) error
+ Clients() map[string]client.Client
+}
+
+type clientsPool struct {
+ clients map[string]client.Client
+}
+
+// NewClustersClientsPool initializes a new ClientsPool
+func NewClustersClientsPool() ClientsPool {
+ return &clientsPool{
+ clients: map[string]client.Client{},
+ }
+}
+
+// Add adds a cluster client to the clients pool with the given user impersonation
+func (cp *clientsPool) Add(user *auth.UserPrincipal, cluster Cluster) error {
+ config := &rest.Config{
+ Host: cluster.Server,
+ BearerToken: cluster.BearerToken,
+ TLSClientConfig: cluster.TLSConfig,
+ Impersonate: rest.ImpersonationConfig{
+ UserName: user.ID,
+ Groups: user.Groups,
+ },
+ }
+
+ leafClient, err := client.New(config, client.Options{
+ Scheme: scheme,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create leaf client: %w", err)
+ }
+
+ cp.clients[cluster.Name] = leafClient
+
+ return nil
+}
+
+// Clients returns the clusters clients
+func (cp *clientsPool) Clients() map[string]client.Client {
+ return cp.clients
+}
diff --git a/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go b/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go
new file mode 100644
index 0000000000..735d769f50
--- /dev/null
+++ b/core/clustersmngr/clustersmngrfakes/fake_cluster_fetcher.go
@@ -0,0 +1,117 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package clustersmngrfakes
+
+import (
+ "context"
+ "sync"
+
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
+)
+
+type FakeClusterFetcher struct {
+ FetchStub func(context.Context) ([]clustersmngr.Cluster, error)
+ fetchMutex sync.RWMutex
+ fetchArgsForCall []struct {
+ arg1 context.Context
+ }
+ fetchReturns struct {
+ result1 []clustersmngr.Cluster
+ result2 error
+ }
+ fetchReturnsOnCall map[int]struct {
+ result1 []clustersmngr.Cluster
+ result2 error
+ }
+ invocations map[string][][]interface{}
+ invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeClusterFetcher) Fetch(arg1 context.Context) ([]clustersmngr.Cluster, error) {
+ fake.fetchMutex.Lock()
+ ret, specificReturn := fake.fetchReturnsOnCall[len(fake.fetchArgsForCall)]
+ fake.fetchArgsForCall = append(fake.fetchArgsForCall, struct {
+ arg1 context.Context
+ }{arg1})
+ stub := fake.FetchStub
+ fakeReturns := fake.fetchReturns
+ fake.recordInvocation("Fetch", []interface{}{arg1})
+ fake.fetchMutex.Unlock()
+ if stub != nil {
+ return stub(arg1)
+ }
+ if specificReturn {
+ return ret.result1, ret.result2
+ }
+ return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeClusterFetcher) FetchCallCount() int {
+ fake.fetchMutex.RLock()
+ defer fake.fetchMutex.RUnlock()
+ return len(fake.fetchArgsForCall)
+}
+
+func (fake *FakeClusterFetcher) FetchCalls(stub func(context.Context) ([]clustersmngr.Cluster, error)) {
+ fake.fetchMutex.Lock()
+ defer fake.fetchMutex.Unlock()
+ fake.FetchStub = stub
+}
+
+func (fake *FakeClusterFetcher) FetchArgsForCall(i int) context.Context {
+ fake.fetchMutex.RLock()
+ defer fake.fetchMutex.RUnlock()
+ argsForCall := fake.fetchArgsForCall[i]
+ return argsForCall.arg1
+}
+
+func (fake *FakeClusterFetcher) FetchReturns(result1 []clustersmngr.Cluster, result2 error) {
+ fake.fetchMutex.Lock()
+ defer fake.fetchMutex.Unlock()
+ fake.FetchStub = nil
+ fake.fetchReturns = struct {
+ result1 []clustersmngr.Cluster
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeClusterFetcher) FetchReturnsOnCall(i int, result1 []clustersmngr.Cluster, result2 error) {
+ fake.fetchMutex.Lock()
+ defer fake.fetchMutex.Unlock()
+ fake.FetchStub = nil
+ if fake.fetchReturnsOnCall == nil {
+ fake.fetchReturnsOnCall = make(map[int]struct {
+ result1 []clustersmngr.Cluster
+ result2 error
+ })
+ }
+ fake.fetchReturnsOnCall[i] = struct {
+ result1 []clustersmngr.Cluster
+ result2 error
+ }{result1, result2}
+}
+
+func (fake *FakeClusterFetcher) Invocations() map[string][][]interface{} {
+ fake.invocationsMutex.RLock()
+ defer fake.invocationsMutex.RUnlock()
+ fake.fetchMutex.RLock()
+ defer fake.fetchMutex.RUnlock()
+ copiedInvocations := map[string][][]interface{}{}
+ for key, value := range fake.invocations {
+ copiedInvocations[key] = value
+ }
+ return copiedInvocations
+}
+
+func (fake *FakeClusterFetcher) recordInvocation(key string, args []interface{}) {
+ fake.invocationsMutex.Lock()
+ defer fake.invocationsMutex.Unlock()
+ if fake.invocations == nil {
+ fake.invocations = map[string][][]interface{}{}
+ }
+ if fake.invocations[key] == nil {
+ fake.invocations[key] = [][]interface{}{}
+ }
+ fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ clustersmngr.ClusterFetcher = new(FakeClusterFetcher)
diff --git a/core/clustersmngr/middleware.go b/core/clustersmngr/middleware.go
new file mode 100644
index 0000000000..b68986ca67
--- /dev/null
+++ b/core/clustersmngr/middleware.go
@@ -0,0 +1,50 @@
+package clustersmngr
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/weaveworks/weave-gitops/pkg/server/auth"
+)
+
+// WithClustersClients creates clusters client for provided user in the context
+func WithClustersClients(clustersFetcher ClusterFetcher, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ user := auth.Principal(r.Context())
+ if user == nil {
+ next.ServeHTTP(w, r)
+ return
+ }
+
+ clusters, err := clustersFetcher.Fetch(r.Context())
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintln(w, "failed fetching clusters list: %w", err)
+ return
+ }
+
+ clientsPool := NewClustersClientsPool()
+ for _, c := range clusters {
+ if err := clientsPool.Add(user, c); err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ fmt.Fprintf(w, "failed adding cluster client to the pool: %s", err)
+ return
+ }
+ }
+
+ ctx := context.WithValue(r.Context(), ClustersClientsPoolCtxKey, clientsPool)
+
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+// ClientsPoolFromCtx returns the ClusterClients pool stored in the context
+func ClientsPoolFromCtx(ctx context.Context) ClientsPool {
+ pool, ok := ctx.Value(ClustersClientsPoolCtxKey).(*clientsPool)
+ if ok {
+ return pool
+ }
+
+ return nil
+}
diff --git a/core/clustersmngr/middleware_test.go b/core/clustersmngr/middleware_test.go
new file mode 100644
index 0000000000..7f91054a3b
--- /dev/null
+++ b/core/clustersmngr/middleware_test.go
@@ -0,0 +1,66 @@
+package clustersmngr_test
+
+import (
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ . "github.com/onsi/gomega"
+
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
+ "github.com/weaveworks/weave-gitops/core/clustersmngr/clustersmngrfakes"
+ "github.com/weaveworks/weave-gitops/pkg/server/auth"
+)
+
+func TestWithClustersClientsMiddleware(t *testing.T) {
+ cluster := makeLeafCluster(t)
+ clustersFetcher := &clustersmngrfakes.FakeClusterFetcher{}
+ clustersFetcher.FetchReturns([]clustersmngr.Cluster{cluster}, nil)
+
+ g := NewGomegaWithT(t)
+
+ defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+ middleware := func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ clientsPool := clustersmngr.ClientsPoolFromCtx(r.Context())
+
+ g.Expect(clientsPool.Clients()).To(HaveKey(cluster.Name))
+
+ next.ServeHTTP(w, r)
+ })
+ }(defaultHandler)
+
+ middleware = clustersmngr.WithClustersClients(clustersFetcher, middleware)
+ middleware = authMiddleware(middleware)
+
+ req := httptest.NewRequest(http.MethodGet, "http://www.foo.com/", nil)
+ res := httptest.NewRecorder()
+ middleware.ServeHTTP(res, req)
+
+ g.Expect(res).To(HaveHTTPStatus(http.StatusOK))
+}
+
+func TestWithClustersClientsMiddlewareFailsToFetchCluster(t *testing.T) {
+ defaultHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
+
+ clustersFetcher := &clustersmngrfakes.FakeClusterFetcher{}
+ clustersFetcher.FetchReturns(nil, errors.New("error"))
+
+ middleware := clustersmngr.WithClustersClients(clustersFetcher, defaultHandler)
+ middleware = authMiddleware(middleware)
+
+ req := httptest.NewRequest(http.MethodGet, "http://www.foo.com/", nil)
+ res := httptest.NewRecorder()
+ middleware.ServeHTTP(res, req)
+
+ g := NewGomegaWithT(t)
+
+ g.Expect(res).To(HaveHTTPStatus(http.StatusInternalServerError))
+}
+
+func authMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ next.ServeHTTP(w, r.WithContext(auth.WithPrincipal(r.Context(), &auth.UserPrincipal{ID: "user@weave.gitops", Groups: []string{"developers"}})))
+ })
+}
diff --git a/core/clustersmngr/single_fetcher.go b/core/clustersmngr/single_fetcher.go
new file mode 100644
index 0000000000..598dcadcd3
--- /dev/null
+++ b/core/clustersmngr/single_fetcher.go
@@ -0,0 +1,28 @@
+package clustersmngr
+
+import (
+ "context"
+
+ "k8s.io/client-go/rest"
+)
+
+type singleClusterFetcher struct {
+ restConfig *rest.Config
+}
+
+func NewSingleClusterFetcher(config *rest.Config) (ClusterFetcher, error) {
+ return singleClusterFetcher{
+ restConfig: config,
+ }, nil
+}
+
+func (cf singleClusterFetcher) Fetch(ctx context.Context) ([]Cluster, error) {
+ return []Cluster{
+ {
+ Name: "Default",
+ Server: cf.restConfig.Host,
+ BearerToken: cf.restConfig.BearerToken,
+ TLSConfig: cf.restConfig.TLSClientConfig,
+ },
+ }, nil
+}
diff --git a/core/clustersmngr/single_fetcher_test.go b/core/clustersmngr/single_fetcher_test.go
new file mode 100644
index 0000000000..07e66812fb
--- /dev/null
+++ b/core/clustersmngr/single_fetcher_test.go
@@ -0,0 +1,29 @@
+package clustersmngr_test
+
+import (
+ "context"
+ "testing"
+
+ . "github.com/onsi/gomega"
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
+ "k8s.io/client-go/rest"
+)
+
+func TestSingleFetcher(t *testing.T) {
+ config := &rest.Config{
+ Host: "my-host",
+ BearerToken: "my-token",
+ }
+
+ g := NewGomegaWithT(t)
+
+ fetcher, err := clustersmngr.NewSingleClusterFetcher(config)
+ g.Expect(err).To(BeNil())
+
+ clusters, err := fetcher.Fetch(context.TODO())
+ g.Expect(err).To(BeNil())
+
+ g.Expect(clusters[0].Name).To(Equal("Default"))
+ g.Expect(clusters[0].Server).To(Equal(config.Host))
+ g.Expect(clusters[0].BearerToken).To(Equal(config.BearerToken))
+}
diff --git a/core/clustersmngr/suite_test.go b/core/clustersmngr/suite_test.go
new file mode 100644
index 0000000000..88b099f3bf
--- /dev/null
+++ b/core/clustersmngr/suite_test.go
@@ -0,0 +1,40 @@
+package clustersmngr_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
+ "github.com/weaveworks/weave-gitops/pkg/testutils"
+)
+
+var k8sEnv *testutils.K8sTestEnv
+
+func TestMain(m *testing.M) {
+ os.Setenv("KUBEBUILDER_ASSETS", "../../tools/bin/envtest")
+
+ var err error
+ k8sEnv, err = testutils.StartK8sTestEnvironment([]string{
+ "../../manifests/crds",
+ "../../tools/testcrds",
+ })
+
+ if err != nil {
+ panic(err)
+ }
+
+ code := m.Run()
+
+ k8sEnv.Stop()
+
+ os.Exit(code)
+}
+
+func makeLeafCluster(t *testing.T) clustersmngr.Cluster {
+ return clustersmngr.Cluster{
+ Name: "leaf-cluster",
+ Server: k8sEnv.Rest.Host,
+ BearerToken: k8sEnv.Rest.BearerToken,
+ TLSConfig: k8sEnv.Rest.TLSClientConfig,
+ }
+}
diff --git a/core/server/kustomization.go b/core/server/kustomization.go
index 750268c9f7..6923060d62 100644
--- a/core/server/kustomization.go
+++ b/core/server/kustomization.go
@@ -2,34 +2,40 @@ package server
import (
"context"
+ "errors"
"fmt"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
"github.com/weaveworks/weave-gitops/core/server/types"
pb "github.com/weaveworks/weave-gitops/pkg/api/core"
)
func (cs *coreServer) ListKustomizations(ctx context.Context, msg *pb.ListKustomizationsRequest) (*pb.ListKustomizationsResponse, error) {
- k8s, err := cs.k8s.Client(ctx)
- if err != nil {
- return nil, doClientError(err)
- }
-
- l := &kustomizev1.KustomizationList{}
-
- if err := list(ctx, k8s, temporarilyEmptyAppName, msg.Namespace, l); err != nil {
- return nil, err
+ clientsPool := clustersmngr.ClientsPoolFromCtx(ctx)
+ if clientsPool == nil {
+ return &pb.ListKustomizationsResponse{
+ Kustomizations: []*pb.Kustomization{},
+ }, errors.New("no clients pool present in context")
}
var results []*pb.Kustomization
- for _, kustomization := range l.Items {
- k, err := types.KustomizationToProto(&kustomization)
- if err != nil {
- return nil, fmt.Errorf("converting items: %w", err)
+ //TODO: handle failures and parallelize
+ for _, c := range clientsPool.Clients() {
+ l := &kustomizev1.KustomizationList{}
+ if err := list(ctx, c, temporarilyEmptyAppName, msg.Namespace, l); err != nil {
+ return nil, err
}
- results = append(results, k)
+ for _, kustomization := range l.Items {
+ k, err := types.KustomizationToProto(&kustomization)
+ if err != nil {
+ return nil, fmt.Errorf("converting items: %w", err)
+ }
+
+ results = append(results, k)
+ }
}
return &pb.ListKustomizationsResponse{
diff --git a/core/server/server.go b/core/server/server.go
index 0d9aa0ca20..f4e918ffa5 100644
--- a/core/server/server.go
+++ b/core/server/server.go
@@ -36,19 +36,19 @@ type coreServer struct {
type CoreServerConfig struct {
Logger logr.Logger
- restCfg *rest.Config
+ RestCfg *rest.Config
clusterName string
}
func NewCoreConfig(cfg *rest.Config, clusterName string) CoreServerConfig {
return CoreServerConfig{
- restCfg: cfg,
+ RestCfg: cfg,
clusterName: clusterName,
}
}
func NewCoreServer(cfg CoreServerConfig) pb.CoreServer {
- cfgGetter := kube.NewImpersonatingConfigGetter(cfg.restCfg, false)
+ cfgGetter := kube.NewImpersonatingConfigGetter(cfg.RestCfg, false)
return &coreServer{
k8s: kube.NewDefaultClientGetter(cfgGetter, cfg.clusterName),
diff --git a/core/server/suite_test.go b/core/server/suite_test.go
index 002e0e7fa8..da5ec0b6d0 100644
--- a/core/server/suite_test.go
+++ b/core/server/suite_test.go
@@ -6,8 +6,10 @@ import (
"os"
"testing"
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
"github.com/weaveworks/weave-gitops/core/server"
pb "github.com/weaveworks/weave-gitops/pkg/api/core"
+ "github.com/weaveworks/weave-gitops/pkg/server/auth"
"github.com/weaveworks/weave-gitops/pkg/testutils"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
@@ -38,7 +40,7 @@ func TestMain(m *testing.M) {
}
func makeGRPCServer(cfg *rest.Config, t *testing.T) pb.CoreClient {
- s := grpc.NewServer()
+ s := grpc.NewServer(withClientsPoolInterceptor(cfg, &auth.UserPrincipal{}))
coreCfg := server.NewCoreConfig(cfg, "foobar")
core := server.NewCoreServer(coreCfg)
@@ -74,3 +76,23 @@ func makeGRPCServer(cfg *rest.Config, t *testing.T) pb.CoreClient {
return pb.NewCoreClient(conn)
}
+
+func withClientsPoolInterceptor(config *rest.Config, user *auth.UserPrincipal) grpc.ServerOption {
+ return grpc.UnaryInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+ cluster := clustersmngr.Cluster{
+ Name: "Default",
+ Server: config.Host,
+ BearerToken: config.BearerToken,
+ TLSConfig: config.TLSClientConfig,
+ }
+
+ clientsPool := clustersmngr.NewClustersClientsPool()
+ if err := clientsPool.Add(user, cluster); err != nil {
+ return nil, err
+ }
+
+ ctx = context.WithValue(ctx, clustersmngr.ClustersClientsPoolCtxKey, clientsPool)
+
+ return handler(ctx, req)
+ })
+}
diff --git a/pkg/server/handler.go b/pkg/server/handler.go
index 4d9314a560..844264ba4a 100644
--- a/pkg/server/handler.go
+++ b/pkg/server/handler.go
@@ -7,6 +7,7 @@ import (
"os"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
+ "github.com/weaveworks/weave-gitops/core/clustersmngr"
core "github.com/weaveworks/weave-gitops/core/server"
pbapp "github.com/weaveworks/weave-gitops/pkg/api/applications"
pbprofiles "github.com/weaveworks/weave-gitops/pkg/api/profiles"
@@ -42,6 +43,12 @@ func NewHandlers(ctx context.Context, cfg *Config) (http.Handler, error) {
httpHandler = middleware.WithProviderToken(cfg.AppConfig.JwtClient, httpHandler, cfg.AppConfig.Logger)
if AuthEnabled() {
+ clustersFetcher, err := clustersmngr.NewSingleClusterFetcher(cfg.CoreServerConfig.RestCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed fetching clusters: %w", err)
+ }
+
+ httpHandler = clustersmngr.WithClustersClients(clustersFetcher, httpHandler)
httpHandler = auth.WithAPIAuth(httpHandler, cfg.AuthServer, PublicRoutes)
}
diff --git a/ui/components/SearchField.tsx b/ui/components/SearchField.tsx
index c6b972aa45..c2f6e09805 100644
--- a/ui/components/SearchField.tsx
+++ b/ui/components/SearchField.tsx
@@ -7,7 +7,6 @@ import Input from "./Input";
type Props = {
className?: string;
-
onSubmit: (val: string) => void;
};
diff --git a/ui/components/SearchInput.tsx b/ui/components/SearchInput.tsx
deleted file mode 100644
index 5b39eb61fb..0000000000
--- a/ui/components/SearchInput.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as React from "react";
-import styled from "styled-components";
-import Button from "./Button";
-import Flex from "./Flex";
-import Icon, { IconType } from "./Icon";
-import Input from "./Input";
-import Spacer from "./Spacer";
-
-export interface Props {
- className?: string;
- /** function to store input in parent component state */
- setSearch: (value: string) => void;
- /** customizable text for input placeholder */
- placeholder?: string;
-}
-
-const CollapsibleInput = styled(Input)`
- max-width: 0px;
- overflow: hidden;
- transition: max-width 0.5s ease;
- &.show {
- max-width: 250px;
- }
-`;
-
-function SearchInput({ className, setSearch, placeholder = "SEARCH" }: Props) {
- const [show, setShow] = React.useState(false);
- return (
-
- ) =>
- setSearch(e.target.value)
- }
- placeholder={placeholder}
- />
-
-
-
- );
-}
-
-export default styled(SearchInput).attrs({ className: SearchInput.name })``;
diff --git a/ui/stories/SearchInput.stories.tsx b/ui/stories/SearchInput.stories.tsx
deleted file mode 100644
index 7d470fe19d..0000000000
--- a/ui/stories/SearchInput.stories.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Meta, Story } from "@storybook/react";
-import React from "react";
-import SearchInput, { Props } from "../components/SearchInput";
-
-export default {
- title: "SearchInput",
- component: SearchInput,
- parameters: {
- docs: {
- description: {
- component:
- "Series of deletable MUI Chip components: https://mui.com/components/chips/",
- },
- },
- },
-} as Meta;
-
-const Template: Story = (args) => {
- return ;
-};
-
-export const Default = Template.bind({});
-Default.args = {};