diff --git a/featureflags/__init__.py b/featureflags/__init__.py
index 72f26f5..0b2f79d 100644
--- a/featureflags/__init__.py
+++ b/featureflags/__init__.py
@@ -1 +1 @@
-__version__ = "1.1.2"
+__version__ = "1.1.3"
diff --git a/featureflags/graph/actions.py b/featureflags/graph/actions.py
index 8824ca2..0370db1 100644
--- a/featureflags/graph/actions.py
+++ b/featureflags/graph/actions.py
@@ -489,13 +489,43 @@ async def update_value_changelog(
@auth_required
@track
async def delete_variable(
- variable_id: str, *, conn: SAConnection,
+ variable_id: str,
+ *,
+ conn: SAConnection,
) -> None:
assert variable_id, "Variable id is required"
variable_uuid = UUID(hex=variable_id)
await conn.execute(
- Variable.__table__.delete().where(
- Variable.id == variable_uuid
+ Variable.__table__.delete().where(Variable.id == variable_uuid)
+ )
+
+
+@auth_required
+@track
+async def delete_project(
+ project_id: str,
+ *,
+ conn: SAConnection,
+) -> None:
+ assert project_id, "Project id is required"
+
+ project_uuid = UUID(hex=project_id)
+
+ variables = await conn.execute(
+ select([Variable.id]).where(Project.id == project_uuid)
+ )
+ variable_ids = [v.id for v in await variables.fetchall()]
+
+ if variable_ids:
+ await conn.execute(
+ Check.__table__.delete().where(Check.variable.in_(variable_ids))
)
+
+ await conn.execute(
+ Variable.__table__.delete().where(Variable.project == project_uuid)
+ )
+
+ await conn.execute(
+ Project.__table__.delete().where(Project.id == project_uuid)
)
diff --git a/featureflags/graph/graph.py b/featureflags/graph/graph.py
index 1d3bade..8cbdbb2 100644
--- a/featureflags/graph/graph.py
+++ b/featureflags/graph/graph.py
@@ -54,6 +54,7 @@
SaveFlagResult,
SaveValueResult,
DeleteVariableResult,
+ DeleteProjectResult,
)
from featureflags.graph.utils import is_valid_uuid
from featureflags.metrics import wrap_metric
@@ -170,9 +171,7 @@ async def root_values(ctx: dict, options: dict) -> list:
)
if value_name:
- expr = (
- expr.where(Value.name.ilike(f"%{value_name}%"))
- )
+ expr = expr.where(Value.name.ilike(f"%{value_name}%"))
return await exec_expression(ctx[GraphContext.DB_ENGINE], expr)
@@ -867,6 +866,28 @@ def get_field(name: str) -> str | None:
],
)
+
+async def delete_project_info(
+ fields: list[Field], results: list[DeleteProjectResult]
+) -> list[list]:
+ [result] = results
+
+ def get_field(name: str) -> str | None:
+ if name == "error":
+ return result.error
+
+ raise ValueError(f"Unknown field: {name}")
+
+ return [[get_field(f.name)] for f in fields]
+
+
+DeleteProjectNode = Node(
+ "DeleteProject",
+ [
+ Field("error", None, delete_project_info),
+ ],
+)
+
GRAPH = Graph(
[
ProjectNode,
@@ -1161,6 +1182,36 @@ async def delete_variable(ctx: dict, options: dict) -> DeleteVariableResult:
return DeleteVariableResult(None)
+@pass_context
+async def delete_project(ctx: dict, options: dict) -> DeleteProjectResult:
+ async with ctx[GraphContext.DB_ENGINE].acquire() as conn:
+ project_uuid = UUID(options["id"])
+
+ is_flags_exists = await exec_scalar(
+ ctx[GraphContext.DB_ENGINE],
+ (select([Flag.id]).where(Flag.project == project_uuid).limit(1)),
+ )
+ if is_flags_exists:
+ return DeleteProjectResult("You need delete all Flags firstly.")
+
+ is_values_exists = await exec_scalar(
+ ctx[GraphContext.DB_ENGINE],
+ (select([Value.id]).where(Value.project == project_uuid).limit(1)),
+ )
+ if is_values_exists:
+ return DeleteProjectResult("You need delete all Values firstly.")
+
+ try:
+ await actions.delete_project(
+ options["id"],
+ conn=conn,
+ )
+ except Exception as e:
+ return DeleteProjectResult(str(e))
+
+ return DeleteProjectResult(None)
+
+
mutation_data_types = {
"SaveFlagOperation": Record[{"type": String, "payload": Any}],
"SaveValueOperation": Record[{"type": String, "payload": Any}],
@@ -1178,6 +1229,7 @@ async def delete_variable(ctx: dict, options: dict) -> DeleteVariableResult:
DeleteFlagNode,
DeleteValueNode,
DeleteVariableNode,
+ DeleteProjectNode,
Root(
[
Link(
@@ -1249,6 +1301,13 @@ async def delete_variable(ctx: dict, options: dict) -> DeleteVariableResult:
options=[Option("id", String)],
requires=None,
),
+ Link(
+ "deleteProject",
+ TypeRef["DeleteProject"],
+ delete_project,
+ options=[Option("id", String)],
+ requires=None,
+ ),
]
),
],
diff --git a/featureflags/graph/types.py b/featureflags/graph/types.py
index 51c50d8..534c9c1 100644
--- a/featureflags/graph/types.py
+++ b/featureflags/graph/types.py
@@ -206,3 +206,7 @@ class DeleteValueResult(NamedTuple):
class DeleteVariableResult(NamedTuple):
error: str | None = None
+
+
+class DeleteProjectResult(NamedTuple):
+ error: str | None = None
diff --git a/ui/src/Base.jsx b/ui/src/Base.jsx
index 46cf8e7..5bd1666 100644
--- a/ui/src/Base.jsx
+++ b/ui/src/Base.jsx
@@ -23,12 +23,7 @@ function Base({ children }) {
const navigate = useNavigate();
const queryParams = new URLSearchParams(location.search);
- const tab = queryParams.get('tab') || "flags";
-
- const setTabToUrl = (name) => {
- queryParams.set('tab', name);
- navigate(`/?${queryParams.toString()}`);
- }
+ const tab = queryParams.get('tab') === 'values' ? 'values' : 'flags';
const handleSearchTermChange = (e) => {
const value = e.target.value;
@@ -43,6 +38,7 @@ function Base({ children }) {
const setSearchTermToUrl = (e) => {
const value = e.target.value;
queryParams.set('term', value);
+ queryParams.set('tab', tab);
navigate(`/?${queryParams.toString()}`);
};
@@ -79,10 +75,9 @@ function Base({ children }) {
>
FeatureFlags
-
-
+
{tab && (
)}
-
-
-
+
{auth.authenticated &&