Skip to content

Commit

Permalink
added button with history of flag/value (#39)
Browse files Browse the repository at this point in the history
* added button with history of flag/value

---------

Co-authored-by: d.maximchuk <[email protected]>
  • Loading branch information
briefausde and d.maximchuk authored Sep 12, 2024
1 parent c78ac63 commit 5e0e34e
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 24 deletions.
2 changes: 1 addition & 1 deletion featureflags/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.1.0"
__version__ = "1.1.1"
66 changes: 66 additions & 0 deletions featureflags/graph/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,52 @@ async def value_project(ids: list[int]) -> list[int]:
return ids


@pass_context
async def get_flag_last_action_timestamp(
ctx: dict, fields: list[Field]
) -> list[str | None]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

[field] = fields
opts = field.options
flag_id = UUID(opts["id"])

result = await exec_scalar(
ctx[GraphContext.DB_ENGINE],
(
select([Changelog.timestamp])
.where(Changelog.flag == flag_id)
.order_by(Changelog.timestamp.desc())
.limit(1)
),
)
return [str(result) if result else None]


@pass_context
async def get_value_last_action_timestamp(
ctx: dict, fields: list[Field]
) -> list[str | None]:
if not ctx[GraphContext.USER_SESSION].is_authenticated:
return []

[field] = fields
opts = field.options
value_id = UUID(opts["id"])

result = await exec_scalar(
ctx[GraphContext.DB_ENGINE],
(
select([ValueChangelog.timestamp])
.where(ValueChangelog.value == value_id)
.order_by(ValueChangelog.timestamp.desc())
.limit(1)
),
)
return [str(result) if result else None]


ID_FIELD = Field("id", None, id_field)

flag_fq = FieldsQuery(GraphContext.DB_ENGINE, Flag.__table__)
Expand All @@ -260,6 +306,8 @@ async def value_project(ids: list[int]) -> list[int]:
Field("name", None, flag_fq),
Field("project", None, flag_fq),
Field("enabled", None, flag_fq),
Field("created_timestamp", None, flag_fq),
Field("reported_timestamp", None, flag_fq),
],
)

Expand All @@ -274,6 +322,8 @@ async def value_project(ids: list[int]) -> list[int]:
Field("enabled", None, value_fq),
Field("value_default", None, value_fq),
Field("value_override", None, value_fq),
Field("created_timestamp", None, value_fq),
Field("reported_timestamp", None, value_fq),
],
)

Expand Down Expand Up @@ -413,6 +463,8 @@ async def value_project(ids: list[int]) -> list[int]:
None,
flag_sg.c(if_some([S.enabled, S.this.enabled], True, False)),
),
Field("created_timestamp", None, flag_sg),
Field("reported_timestamp", None, flag_sg),
],
)

Expand Down Expand Up @@ -449,6 +501,8 @@ async def value_project(ids: list[int]) -> list[int]:
),
Field("value_default", None, value_sg),
Field("value_override", None, value_sg),
Field("created_timestamp", None, value_sg),
Field("reported_timestamp", None, value_sg),
],
)

Expand Down Expand Up @@ -538,6 +592,18 @@ async def value_project(ids: list[int]) -> list[int]:

RootNode = Root(
[
Field(
"flagLastActionTimestamp",
Optional[String],
get_flag_last_action_timestamp,
options=[Option("id", String)],
),
Field(
"valueLastActionTimestamp",
Optional[String],
get_value_last_action_timestamp,
options=[Option("id", String)],
),
Link(
"flag",
Optional["Flag"],
Expand Down
23 changes: 23 additions & 0 deletions featureflags/http/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
notify server about new projects/variables/flags
TODO: refactor.
"""
from datetime import datetime
from uuid import UUID, uuid4

from aiopg.sa import SAConnection
Expand Down Expand Up @@ -129,6 +130,14 @@ async def _insert_flag(
return await result.scalar()


async def _update_flag_report_timestamp(flag_id: UUID, *, conn: SAConnection):
await conn.execute(
Flag.__table__.update()
.where(Flag.id == flag_id)
.values({Flag.reported_timestamp: datetime.utcnow()})
)


async def _get_or_create_flag(
project: UUID,
flag: str,
Expand All @@ -146,6 +155,9 @@ async def _get_or_create_flag(
id_ = await _select_flag(project, flag, conn=conn)
assert id_ is not None # must be in db
entity_cache.flag[project][flag] = id_

await _update_flag_report_timestamp(id_, conn=conn)

return id_


Expand Down Expand Up @@ -187,6 +199,14 @@ async def _insert_value(
return await result.scalar()


async def _update_value_report_timestamp(value_id: UUID, *, conn: SAConnection):
await conn.execute(
Value.__table__.update()
.where(Value.id == value_id)
.values({Value.reported_timestamp: datetime.utcnow()})
)


async def _get_or_create_value(
project: UUID,
value: str,
Expand All @@ -205,6 +225,9 @@ async def _get_or_create_value(
id_ = await _select_value(project, value, conn=conn)
assert id_ is not None # must be in db
entity_cache.value[project][value] = id_

await _update_value_report_timestamp(id_, conn=conn)

return id_


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import sqlalchemy as sa

from alembic import op
from sqlalchemy.dialects import postgresql


revision = "4d42cf3d11de"
down_revision = "1876f90b58e8"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"flag",
sa.Column("created_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"flag",
sa.Column("reported_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"value",
sa.Column("created_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
op.add_column(
"value",
sa.Column("reported_timestamp", postgresql.TIMESTAMP(), nullable=True),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("value", "reported_timestamp")
op.drop_column("value", "created_timestamp")
op.drop_column("flag", "reported_timestamp")
op.drop_column("flag", "created_timestamp")
# ### end Alembic commands ###
14 changes: 14 additions & 0 deletions featureflags/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ class Flag(Base):
id = Column(UUID(as_uuid=True), primary_key=True)
name = Column(String, nullable=False)
enabled = Column(Boolean)
created_timestamp = Column(
TIMESTAMP,
default=datetime.utcnow(),
nullable=True,
)
reported_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)

project: UUID = Column(ForeignKey("project.id"), nullable=False)

Expand Down Expand Up @@ -193,6 +201,12 @@ class Value(Base):
enabled = Column(Boolean)
value_default = Column(String, nullable=False)
value_override = Column(String, nullable=False)
created_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)
reported_timestamp = Column(
TIMESTAMP, default=datetime.utcnow(), nullable=True
)

project: UUID = Column(ForeignKey("project.id"), nullable=False)

Expand Down
86 changes: 74 additions & 12 deletions ui/src/Dashboard/Flag.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Divider,
Popconfirm,
message,
Modal,
} from 'antd';
import { useEffect, useState } from 'react';
import {
Expand All @@ -26,12 +27,14 @@ import './Flag.less';
import {
FlagContext,
useFlagState,
useProject
useProject,
} from './context';
import { Conditions } from './Conditions';
import { TYPES, KIND_TO_TYPE, KIND, TYPE_TO_KIND } from './constants';
import { useActions } from './actions';
import { copyToClipboard, replaceValueInArray } from './utils';
import { copyToClipboard, formatTimestamp, replaceValueInArray } from './utils';
import { useLazyQuery } from "@apollo/client";
import { FLAG_LAST_ACTION_TIMESTAMP_QUERY } from "./queries";


const ResetButton = ({ onClick, disabled }) => {
Expand Down Expand Up @@ -105,20 +108,72 @@ const Buttons = ({ onReset, onCancel, onSave, onDelete, onToggle }) => {
);
}

const FlagName = ({ name }) => {
const FlagTitle = ({ name, flagId, createdTimestamp, reportedTimestamp }) => {
const [ isModalVisible, setIsModalVisible ] = useState(false);
const [ flagHistory, setFlagHistory ] = useState({
lastAction: "Loading...",
});

const [ loadLastActionTimestamp ] = useLazyQuery(FLAG_LAST_ACTION_TIMESTAMP_QUERY, {
fetchPolicy: "network-only",
variables: { id: flagId },
onCompleted: (data) => {
setFlagHistory({ lastAction: `${data?.flagLastActionTimestamp || "N/A"}` });
},
onError: () => {
message.error("Error fetching last action");
setFlagHistory({ lastAction: "N/A", });
},
});

const getFlagHistory = () => {
loadLastActionTimestamp();
setIsModalVisible(true);
};

const handleOk = () => {
setIsModalVisible(false);
};

const copyFlag = () => {
copyToClipboard(name, `Flag ${name} copied to clipboard`);
}

const TimestampRow = ({ label, timestamp }) => (
<p>
{label}: <b style={{ color: 'green' }}>{formatTimestamp(timestamp)}</b>
</p>
);

return (
<div
className='flag-name'
onClick={copyFlag}
>
<Space size={8}>
<CopyOutlined />
{name}
</Space>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div
className='flag-name'
onClick={copyFlag}
>
<Space size={8}>
<CopyOutlined />
{name}
</Space>
</div>
<Button onClick={getFlagHistory}>
HISTORY
</Button>
<Modal
title="Flag History"
visible={isModalVisible}
onOk={handleOk}
onCancel={handleOk}
footer={[
<Button key="ok" type="primary" onClick={handleOk}>
OK
</Button>,
]}
>
<TimestampRow label="Created" timestamp={createdTimestamp} />
<TimestampRow label="Last Reported" timestamp={reportedTimestamp} />
<TimestampRow label="Last Action" timestamp={flagHistory.lastAction} />
</Modal>
</div>
)
}
Expand All @@ -131,6 +186,8 @@ const getInitialFlagState = (flag) => ({
enabled: flag.enabled,
// TODO sort conditions, because after save, the order is not guaranteed now
conditions: flag.conditions.map((c) => c.id),
createdTimestamp: flag.created_timestamp,
reportedTimestamp: flag.reported_timestamp,
});

const getInitialConditions = (flag) => {
Expand Down Expand Up @@ -403,7 +460,12 @@ export const Flag = ({ flag }) => {
<Card
size="small"
className={saveFlagFailed ? 'invalid' : ''}
title={<FlagName name={flag.name}/>}
title={<FlagTitle
name={flag.name}
flagId={flag.id}
createdTimestamp={flag.createdTimestamp}
reportedTimestamp={flag.reportedTimestamp}
/>}
style={{ width: 800, borderRadius: '5px' }}
>
<FlagContext.Provider value={ctx}>
Expand Down
Loading

0 comments on commit 5e0e34e

Please sign in to comment.